Simplify payment confirmation screen (#1029)
* update translations * simplifying payments display * remove unconsistant icons * adding translations * fix usdb value display * fix tokens bug
This commit is contained in:
+102
-21
@@ -23,6 +23,8 @@ jest.mock('../app/constants', () => {
|
||||
return {
|
||||
...theme,
|
||||
CENTER: {},
|
||||
ICONS: {},
|
||||
USDB_TOKEN_ID: 'usdb-token-id',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,6 +37,22 @@ jest.mock('react-native-email-link', () => ({
|
||||
openComposer: jest.fn(),
|
||||
}));
|
||||
|
||||
// Pulled in transitively by the recipient card. Ships untranspiled ESM.
|
||||
jest.mock('expo-image', () => ({ Image: () => null }));
|
||||
jest.mock('react-native-country-flag', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
// The recipient pill's labels come from `i18next.t` directly (recipientCard is
|
||||
// shared with the pre-send screen, which is not a hook context).
|
||||
jest.mock('i18next', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions', () => ({
|
||||
copyToClipboard: jest.fn(),
|
||||
}));
|
||||
@@ -49,11 +67,6 @@ jest.mock('../app/functions/lrc20/formatTokensBalance', () => ({
|
||||
default: (amount, decimals) => `tokens-${amount}-${decimals}`,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/displayCorrectDenomination', () => ({
|
||||
__esModule: true,
|
||||
default: ({ amount }) => `denom-${amount}`,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/customUUID', () => ({
|
||||
__esModule: true,
|
||||
default: () => 'uuid',
|
||||
@@ -78,6 +91,7 @@ jest.mock('../app/functions/lnurl', () => ({
|
||||
|
||||
jest.mock('../app/functions/sendBitcoin/getPhonePaymentAddress', () => ({
|
||||
canonicalizePhonePaymentAddress: value => value,
|
||||
getPhonePaymentDisplay: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('../app/hooks/themeColors', () => () => ({
|
||||
@@ -101,19 +115,6 @@ jest.mock('../context-store/appStatus', () => ({
|
||||
useAppStatus: () => ({ screenDimensions: { width: 400, height: 800 } }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/context', () => ({
|
||||
useGlobalContextProvider: () => ({
|
||||
masterInfoObject: {
|
||||
fiatCurrency: 'USD',
|
||||
userBalanceDenomination: 'sats',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/nodeContext', () => ({
|
||||
useNodeContext: () => ({ fiatStats: { coin: 'USD', value: 100000000 } }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/globalContacts', () => ({
|
||||
useGlobalContactsInfo: () => ({ decodedAddedContacts: [] }),
|
||||
}));
|
||||
@@ -189,13 +190,24 @@ function balanceInputProps(renderer) {
|
||||
return nodes.length ? nodes[0].props.componentProps : null;
|
||||
}
|
||||
|
||||
function renderedText(renderer) {
|
||||
const RN = require('react-native');
|
||||
return renderer.root
|
||||
.findAllByType(RN.Text)
|
||||
.flatMap(node => node.props.children)
|
||||
.filter(child => typeof child === 'string');
|
||||
}
|
||||
|
||||
function satTextProps(renderer) {
|
||||
const nodes = renderer.root.findAllByProps({ testID: 'formatted-sat-text' });
|
||||
return nodes.length ? nodes[0].props.componentProps : null;
|
||||
}
|
||||
|
||||
// paymentType lives at the top level of a transaction, not in `details` — the
|
||||
// recipient pill reads it from there.
|
||||
const successfulOutgoingTx = {
|
||||
details: { amount: 1500, direction: 'OUTGOING', paymentType: 'lightning' },
|
||||
paymentType: 'lightning',
|
||||
details: { amount: 1500, direction: 'OUTGOING' },
|
||||
};
|
||||
|
||||
const fiatPaymentDisplay = {
|
||||
@@ -256,7 +268,7 @@ describe('ConfirmTxPage amount rendering', () => {
|
||||
expect(props.forceCurrency).toBe('EUR');
|
||||
});
|
||||
|
||||
test('uses token metadata for an LRC20 send rendered through FormattedBalanceInput', async () => {
|
||||
test('uses the send screen ticker for an LRC20 send rendered through FormattedBalanceInput', async () => {
|
||||
mockSparkInformation = {
|
||||
tokens: {
|
||||
'token-id': { tokenMetadata: { tokenTicker: 'USDB', decimals: 6 } },
|
||||
@@ -272,7 +284,12 @@ describe('ConfirmTxPage amount rendering', () => {
|
||||
},
|
||||
},
|
||||
displayAmount: '12.34',
|
||||
paymentDisplay: { denomination: 'fiat', forceCurrency: 'USD', forceFiatStats: null },
|
||||
displayTokenTicker: 'USDB',
|
||||
paymentDisplay: {
|
||||
denomination: 'fiat',
|
||||
forceCurrency: 'USD',
|
||||
forceFiatStats: null,
|
||||
},
|
||||
});
|
||||
|
||||
const props = balanceInputProps(renderer);
|
||||
@@ -280,4 +297,68 @@ describe('ConfirmTxPage amount rendering', () => {
|
||||
expect(props.customCurrencyCode).toBe('USDB');
|
||||
expect(props.maxDecimals).toBe(6);
|
||||
});
|
||||
|
||||
test('keeps the fiat display for a USDB-funded send the send screen showed as fiat', async () => {
|
||||
mockSparkInformation = {
|
||||
tokens: {
|
||||
'token-id': { tokenMetadata: { tokenTicker: 'USDB', decimals: 6 } },
|
||||
},
|
||||
};
|
||||
const renderer = await renderConfirm({
|
||||
transaction: {
|
||||
details: {
|
||||
amount: 5000,
|
||||
direction: 'OUTGOING',
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: 'token-id',
|
||||
},
|
||||
},
|
||||
displayAmount: '12.34',
|
||||
// no displayTokenTicker: the send screen labelled this one "$"
|
||||
paymentDisplay: {
|
||||
denomination: 'fiat',
|
||||
forceCurrency: 'USD',
|
||||
forceFiatStats: null,
|
||||
},
|
||||
});
|
||||
|
||||
const props = balanceInputProps(renderer);
|
||||
expect(props).not.toBeNull();
|
||||
expect(props.customCurrencyCode).toBeFalsy();
|
||||
expect(props.forceCurrency).toBe('USD');
|
||||
expect(props.maxDecimals).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfirmTxPage recipient pill', () => {
|
||||
beforeEach(() => {
|
||||
mockSparkInformation = { tokens: {} };
|
||||
});
|
||||
|
||||
test('names the funding asset for a bitcoin-funded lightning send', async () => {
|
||||
const renderer = await renderConfirm({ transaction: successfulOutgoingTx });
|
||||
|
||||
expect(renderedText(renderer)).toContain(
|
||||
'wallet.sendPages.sendPaymentScreen.lightningPayment',
|
||||
);
|
||||
});
|
||||
|
||||
test('names the dollar balance for a USD-funded bolt11 (recorded as spark)', async () => {
|
||||
const renderer = await renderConfirm({
|
||||
transaction: {
|
||||
paymentType: 'spark',
|
||||
details: {
|
||||
amount: 1500,
|
||||
direction: 'OUTGOING',
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: 'usdb-token-id',
|
||||
destinationChain: 'lightning',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(renderedText(renderer)).toContain(
|
||||
'wallet.sendPages.sendPaymentScreen.dollarPayment',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import getPhonePaymentAddress, {
|
||||
getPhonePaymentCountry,
|
||||
canonicalizePhonePaymentAddress,
|
||||
getPhonePostProvider,
|
||||
getPhonePaymentDisplay,
|
||||
fetchPhonePaymentInvoice,
|
||||
PROVIDER_COUNTRY_CURRENCY,
|
||||
} from '../../../app/functions/sendBitcoin/getPhonePaymentAddress';
|
||||
@@ -335,3 +336,31 @@ describe('fetchPhonePaymentInvoice', () => {
|
||||
await expect(fetchPhonePaymentInvoice(args)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhonePaymentDisplay', () => {
|
||||
it('returns iso code + international-formatted number for a KE provider address', () => {
|
||||
expect(getPhonePaymentDisplay(KE)).toEqual({
|
||||
isoCode: 'KE',
|
||||
formatted: '+254 717 252303',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats a ZM address whose local part is national (0977…)', () => {
|
||||
const res = getPhonePaymentDisplay('0977123456@bitzed.xyz');
|
||||
expect(res.isoCode).toBe('ZM');
|
||||
expect(res.formatted.startsWith('+260')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles a POST-provider (Burundi) address', () => {
|
||||
expect(getPhonePaymentDisplay('25779561234@exchanger.mysatoshis.bi').isoCode).toBe('BI');
|
||||
});
|
||||
|
||||
it('returns null for a non-phone lightning address', () => {
|
||||
expect(getPhonePaymentDisplay('satoshi@walletofsatoshi.com')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for non-string / malformed input', () => {
|
||||
expect(getPhonePaymentDisplay(undefined)).toBeNull();
|
||||
expect(getPhonePaymentDisplay('no-at-sign')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// The recipient pill names AND illustrates the asset that LEFT the wallet, not the
|
||||
// rail it took — no lightning bolts or spark logos for bitcoin/dollar sends. USDB
|
||||
// rides the same LRC20 rails as any other token, so the token id is the only thing
|
||||
// separating "Dollar payment" from "Token payment" — these assertions are what stops
|
||||
// a dollar-funded send from reading "Bitcoin payment" or showing a bolt again.
|
||||
|
||||
const USDB = 'usdb-token-id';
|
||||
|
||||
jest.mock('expo-image', () => ({ Image: () => null }));
|
||||
jest.mock('react-native-country-flag', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
jest.mock('../app/constants', () => ({
|
||||
COLORS: {},
|
||||
ICONS: {},
|
||||
USDB_TOKEN_ID: 'usdb-token-id',
|
||||
}));
|
||||
jest.mock(
|
||||
'../app/components/admin/homeComponents/contacts/internalComponents/profileImage',
|
||||
() => ({ __esModule: true, default: () => null }),
|
||||
);
|
||||
jest.mock('i18next', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
},
|
||||
}));
|
||||
|
||||
const {
|
||||
resolveRecipientDisplay,
|
||||
} = require('../app/components/admin/homeComponents/sendBitcoin/components/recipientCard');
|
||||
|
||||
const LIGHTNING = 'wallet.sendPages.sendPaymentScreen.lightningPayment';
|
||||
const DOLLAR = 'wallet.sendPages.sendPaymentScreen.dollarPayment';
|
||||
const TOKEN = 'wallet.sendPages.sendPaymentScreen.tokenPayment';
|
||||
|
||||
const tx = (paymentType, details = {}) => ({ paymentType, details });
|
||||
|
||||
describe('resolveRecipientDisplay payment asset label', () => {
|
||||
it('labels a bitcoin-funded lightning send "Bitcoin payment"', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('lightning', { isLRC20Payment: false }),
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
kind: 'asset',
|
||||
asset: 'bitcoin',
|
||||
displayName: LIGHTNING,
|
||||
});
|
||||
});
|
||||
|
||||
it('labels a dollar-funded lightning send "Dollar payment"', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('lightning', {
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: USDB,
|
||||
}),
|
||||
});
|
||||
// Asset, not rail: dollars over lightning is a dollar icon, never a bolt.
|
||||
expect(resolved).toMatchObject({
|
||||
kind: 'asset',
|
||||
asset: 'dollar',
|
||||
displayName: DOLLAR,
|
||||
});
|
||||
});
|
||||
|
||||
it('labels a dollar-funded spark send "Dollar payment"', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('spark', { isLRC20Payment: true, LRC20Token: USDB }),
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
kind: 'asset',
|
||||
asset: 'dollar',
|
||||
displayName: DOLLAR,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the dollar icon for a USD-funded bolt11 (recorded as spark)', () => {
|
||||
// payments.js writes this shape: the swap leaves as USDB over spark rails,
|
||||
// but the destination is a bolt11. Either way the user sent dollars.
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('spark', {
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: USDB,
|
||||
destinationChain: 'lightning',
|
||||
}),
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
kind: 'asset',
|
||||
asset: 'dollar',
|
||||
displayName: DOLLAR,
|
||||
});
|
||||
});
|
||||
|
||||
it('labels a non-USDB LRC20 send "Token payment" and keeps the spark logo', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('spark', {
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: 'some-other-token-id',
|
||||
}),
|
||||
});
|
||||
expect(resolved).toMatchObject({ kind: 'spark', displayName: TOKEN });
|
||||
});
|
||||
|
||||
it('labels a bitcoin-funded spark send "Bitcoin payment"', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('spark', { isLRC20Payment: false, LRC20Token: '' }),
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
asset: 'bitcoin',
|
||||
displayName: LIGHTNING,
|
||||
});
|
||||
});
|
||||
|
||||
it('labels an on-chain send "Bitcoin payment"', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('bitcoin'),
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
asset: 'bitcoin',
|
||||
displayName: LIGHTNING,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the dollar icon for a dollar-funded on-chain send', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
transaction: tx('bitcoin', { isLRC20Payment: true, LRC20Token: USDB }),
|
||||
});
|
||||
expect(resolved).toMatchObject({ asset: 'dollar', displayName: DOLLAR });
|
||||
});
|
||||
|
||||
it('names the asset + chain for an external stablecoin, not the spark type', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
// A stablecoin send is also paymentType 'spark' with LRC20Token USDB, so
|
||||
// this only passes while the stablecoin check runs ahead of both.
|
||||
transaction: tx('spark', { isLRC20Payment: true, LRC20Token: USDB }),
|
||||
stablecoinInfo: { asset: 'USDC', label: 'Base' },
|
||||
});
|
||||
expect(resolved.kind).toBe('stablecoin');
|
||||
expect(resolved.displayName).toBe(
|
||||
'screens.inAccount.confirmTxPage.stablecoinDesc:{"asset":"USDC","chain":"Base"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers a named recipient over any asset label', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
contactInfo: { name: 'Satoshi' },
|
||||
transaction: tx('spark', { isLRC20Payment: true, LRC20Token: USDB }),
|
||||
});
|
||||
expect(resolved).toMatchObject({ kind: 'contact', displayName: 'Satoshi' });
|
||||
});
|
||||
|
||||
it('returns null when there is nothing to name', () => {
|
||||
expect(resolveRecipientDisplay({})).toBeNull();
|
||||
});
|
||||
|
||||
it('falls through a contact with no name and no image instead of an empty pill', () => {
|
||||
const resolved = resolveRecipientDisplay({
|
||||
contactInfo: { uuid: 'abc' },
|
||||
transaction: tx('bitcoin'),
|
||||
});
|
||||
expect(resolved).toMatchObject({ kind: 'asset', asset: 'bitcoin' });
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useMemo } from 'react';
|
||||
import { Image } from 'expo-image';
|
||||
import { ThemeText } from '../../../../../functions/CustomElements';
|
||||
import { CENTER, COLORS, FONT, ICONS, SIZES } from '../../../../../constants';
|
||||
import { CENTER, FONT, SIZES } from '../../../../../constants';
|
||||
import GetThemeColors from '../../../../../hooks/themeColors';
|
||||
import formatSparkPaymentAddress from '../functions/formatSparkPaymentAddress';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -10,24 +9,14 @@ import { InputTypes } from 'bitcoin-address-parser';
|
||||
import ContactProfileImage from '../../contacts/internalComponents/profileImage';
|
||||
import normalizeLNURLAddress from '../../../../../functions/lnurl/normalizeLNURLAddress';
|
||||
import ProfileImageRow from '../../contacts/internalComponents/profileImageRow';
|
||||
import ThemeIcon from '../../../../../functions/CustomElements/themeIcon';
|
||||
import { HIDDEN_OPACITY } from '../../../../../constants/theme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// LNURL/lightning-address domain → ICONS key for the provider brand logo.
|
||||
const LNURL_PROVIDER_ICONS = {
|
||||
'aqua.net': 'aqua',
|
||||
'blink.sv': 'blink',
|
||||
'breez.tips': 'breez',
|
||||
'cake.cash': 'cake',
|
||||
'coinos.io': 'coinos',
|
||||
'mannabitcoin.com': 'mannabitcoin',
|
||||
'cluborange.org': 'cluborange',
|
||||
'strike.me': 'strike',
|
||||
'tether.me': 'tether',
|
||||
'walletofsatoshi.com': 'walletofsatoshi',
|
||||
'zeuspay.com': 'zeuspay',
|
||||
};
|
||||
import {
|
||||
paymentAssetLabel,
|
||||
RecipientAvatar,
|
||||
resolveAssetAvatar,
|
||||
resolveRecipientDisplay,
|
||||
} from './recipientCard';
|
||||
import ThemeIcon from '../../../../../functions/CustomElements/themeIcon';
|
||||
|
||||
export default function InvoiceInfo({
|
||||
paymentInfo,
|
||||
@@ -38,13 +27,17 @@ export default function InvoiceInfo({
|
||||
isSplitPayment,
|
||||
splitRecipients = [],
|
||||
isUsingBranta,
|
||||
isDollarBalance,
|
||||
isToken,
|
||||
}) {
|
||||
const formmateedSparkPaymentInfo = formatSparkPaymentAddress(
|
||||
paymentInfo,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
// Labels come from `i18next.t` inside recipientCard, which doesn't subscribe
|
||||
// this component to language changes — the hook does.
|
||||
useTranslation();
|
||||
const { backgroundOffset, backgroundColor } = GetThemeColors();
|
||||
const navigate = useNavigation();
|
||||
const splitContacts = splitRecipients?.map(({ contact }) => contact);
|
||||
@@ -60,23 +53,21 @@ export default function InvoiceInfo({
|
||||
!isSplitPayment &&
|
||||
fromPage !== 'contacts' &&
|
||||
!isLNURLPay &&
|
||||
paymentType !== 'lightning';
|
||||
paymentType !== 'lightning' &&
|
||||
paymentType !== 'spark';
|
||||
const isClickable = !isSplitPayment && !showsFullAddress;
|
||||
|
||||
// LNURL: resolve the human-readable "user@host", match the host to a provider
|
||||
// logo, and drop "@host" when we have a logo (the logo conveys the provider).
|
||||
// LNURL: resolve the human-readable "user@host". The shared resolver picks the
|
||||
// provider brand logo, a mobile-money country flag + formatted phone number, or
|
||||
// the plain address, so the pre-send and post-send screens stay in sync.
|
||||
const normalizedLNURL = isLNURLPay
|
||||
? normalizeLNURLAddress(paymentInfo?.data?.address) ??
|
||||
paymentInfo?.data?.address ??
|
||||
''
|
||||
: '';
|
||||
const lnurlDomain = normalizedLNURL.includes('@')
|
||||
? normalizedLNURL.split('@')[1]?.toLowerCase()
|
||||
: '';
|
||||
const providerIconKey = LNURL_PROVIDER_ICONS[lnurlDomain];
|
||||
const lnurlDisplayText = providerIconKey
|
||||
? normalizedLNURL.split('@')[0]
|
||||
: normalizedLNURL;
|
||||
const lnurlResolved = isLNURLPay
|
||||
? resolveRecipientDisplay({ lnurlAddress: normalizedLNURL })
|
||||
: null;
|
||||
|
||||
// On-chain / spark addresses: 4-char groups with alternating weight for
|
||||
// easy visual validation (mirrors depositQRView).
|
||||
@@ -99,63 +90,44 @@ export default function InvoiceInfo({
|
||||
if (isLNURLPay) {
|
||||
paymentContent = (
|
||||
<View style={styles.contactRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.profileImage,
|
||||
providerIconKey
|
||||
? styles.providerLogoCircle
|
||||
: { backgroundColor: backgroundColor },
|
||||
]}
|
||||
>
|
||||
{providerIconKey ? (
|
||||
<Image
|
||||
style={styles.providerLogo}
|
||||
source={ICONS[providerIconKey]}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<ContactProfileImage
|
||||
updated={undefined}
|
||||
uri={undefined}
|
||||
darkModeType={darkModeType}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<ThemeText
|
||||
styles={styles.addressText}
|
||||
CustomNumberOfLines={1}
|
||||
content={lnurlDisplayText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (paymentType === 'lightning') {
|
||||
paymentContent = (
|
||||
<View style={styles.contactRow}>
|
||||
<View
|
||||
style={[styles.profileImage, { backgroundColor: backgroundColor }]}
|
||||
>
|
||||
<Image
|
||||
style={[
|
||||
styles.lightningIcon,
|
||||
{
|
||||
tintColor:
|
||||
theme && darkModeType ? COLORS.darkModeText : COLORS.primary,
|
||||
},
|
||||
]}
|
||||
source={ICONS.lightningReceiveIcon}
|
||||
contentFit="contain"
|
||||
<View style={styles.avatarSpacing}>
|
||||
<RecipientAvatar
|
||||
resolved={lnurlResolved}
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
</View>
|
||||
<ThemeText
|
||||
styles={styles.addressText}
|
||||
CustomNumberOfLines={1}
|
||||
content={t('wallet.sendPages.sendPaymentScreen.lightningPayment')}
|
||||
content={lnurlResolved?.displayName}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (paymentType === 'lightning' || paymentType === 'spark') {
|
||||
// Same avatar the success screen uses, so the icon can't drift between the
|
||||
// two screens.
|
||||
paymentContent = (
|
||||
<View style={styles.contactRow}>
|
||||
<View style={styles.avatarSpacing}>
|
||||
<RecipientAvatar
|
||||
resolved={resolveAssetAvatar({ isDollarBalance, isToken })}
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
</View>
|
||||
<ThemeText
|
||||
styles={styles.addressText}
|
||||
CustomNumberOfLines={1}
|
||||
content={paymentAssetLabel({ isDollarBalance, isToken })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
// bitcoin / spark / lrc20
|
||||
// bitcoin / spark / lrc20: show the full address so the user can verify it
|
||||
// against what they scanned/pasted before signing.
|
||||
paymentContent = (
|
||||
<ThemeText
|
||||
styles={styles.segmentText}
|
||||
@@ -278,17 +250,8 @@ const styles = StyleSheet.create({
|
||||
overflow: 'hidden',
|
||||
marginRight: 10,
|
||||
},
|
||||
providerLogoCircle: {
|
||||
backgroundColor: COLORS.white,
|
||||
borderColor: COLORS.gray,
|
||||
},
|
||||
providerLogo: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
lightningIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
avatarSpacing: {
|
||||
marginRight: 10,
|
||||
},
|
||||
addressText: {
|
||||
includeFontPadding: false,
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import CountryFlag from 'react-native-country-flag';
|
||||
import { COLORS, ICONS, USDB_TOKEN_ID } from '../../../../../constants';
|
||||
import ContactProfileImage from '../../contacts/internalComponents/profileImage';
|
||||
import { getPhonePaymentDisplay } from '../../../../../functions/sendBitcoin/getPhonePaymentAddress';
|
||||
import i18next from 'i18next';
|
||||
|
||||
// LNURL/lightning-address domain → ICONS key for the provider brand logo. When a
|
||||
// domain matches, the logo conveys the provider so we drop the "@host" and show
|
||||
// just the username.
|
||||
export const LNURL_PROVIDER_ICONS = {
|
||||
'aqua.net': 'aqua',
|
||||
'blink.sv': 'blink',
|
||||
'breez.tips': 'breez',
|
||||
'cake.cash': 'cake',
|
||||
'coinos.io': 'coinos',
|
||||
'mannabitcoin.com': 'mannabitcoin',
|
||||
'cluborange.org': 'cluborange',
|
||||
'strike.me': 'strike',
|
||||
'tether.me': 'tether',
|
||||
'walletofsatoshi.com': 'walletofsatoshi',
|
||||
'zeuspay.com': 'zeuspay',
|
||||
};
|
||||
|
||||
// Names the asset that left the wallet, not the rail it took — a bolt11 paid from
|
||||
// the dollar balance is a "Dollar payment". Dollar wins over token because USDB is
|
||||
// an LRC20 token too.
|
||||
export function paymentAssetLabel({ isDollarBalance, isToken }) {
|
||||
if (isDollarBalance)
|
||||
return i18next.t('wallet.sendPages.sendPaymentScreen.dollarPayment');
|
||||
if (isToken)
|
||||
return i18next.t('wallet.sendPages.sendPaymentScreen.tokenPayment');
|
||||
return i18next.t('wallet.sendPages.sendPaymentScreen.lightningPayment');
|
||||
}
|
||||
|
||||
// The icon follows the label: the user cares what they're sending, not which rail
|
||||
// carries it. Tokens are neither bitcoin nor dollars, so they keep the spark logo.
|
||||
export function resolveAssetAvatar({ isDollarBalance, isToken }) {
|
||||
if (isToken) return { kind: 'spark', logo: 'sparkLogoLight' };
|
||||
return { kind: 'asset', asset: isDollarBalance ? 'dollar' : 'bitcoin' };
|
||||
}
|
||||
|
||||
// Resolves a recipient into everything the avatar + name row needs. Shared by the
|
||||
// pre-send invoice info and the post-send confirmation card so the two never drift.
|
||||
// `lnurlAddress` must already be normalized to `user@host`. Priority: branta →
|
||||
// contact → phone/mobile-money → LNURL provider → plain lightning address →
|
||||
// external stablecoin → the asset label for a raw bolt11/on-chain/spark send.
|
||||
// Returns null when there is nothing at all to name. `displayName` is always
|
||||
// already-translated text, never a key.
|
||||
export function resolveRecipientDisplay({
|
||||
lnurlAddress,
|
||||
contactInfo,
|
||||
brantaName,
|
||||
brantaLogo,
|
||||
transaction,
|
||||
stablecoinInfo,
|
||||
}) {
|
||||
if (brantaName || brantaLogo) {
|
||||
return {
|
||||
kind: 'branta',
|
||||
displayName: brantaName || '',
|
||||
imageUri: brantaLogo || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// A contact with neither a name nor an image would render an empty pill, so
|
||||
// fall through to the address/payment-type branches instead.
|
||||
const contactName = contactInfo?.name || contactInfo?.uniqueName || '';
|
||||
if (contactInfo && (contactName || contactInfo.imageData?.localUri)) {
|
||||
return {
|
||||
kind: 'contact',
|
||||
displayName: contactName,
|
||||
imageUri: contactInfo.imageData?.localUri,
|
||||
imageUpdated: contactInfo.imageData?.updated,
|
||||
};
|
||||
}
|
||||
|
||||
if (lnurlAddress) {
|
||||
const phone = getPhonePaymentDisplay(lnurlAddress);
|
||||
if (phone) {
|
||||
return {
|
||||
kind: 'phone',
|
||||
displayName: phone.formatted,
|
||||
isoCode: phone.isoCode,
|
||||
fullAddress: lnurlAddress,
|
||||
};
|
||||
}
|
||||
|
||||
const domain = lnurlAddress.includes('@')
|
||||
? lnurlAddress.split('@')[1]?.toLowerCase()
|
||||
: '';
|
||||
const providerIconKey = LNURL_PROVIDER_ICONS[domain];
|
||||
return {
|
||||
kind: providerIconKey ? 'provider' : 'lightning',
|
||||
displayName: providerIconKey ? lnurlAddress.split('@')[0] : lnurlAddress,
|
||||
providerIconKey,
|
||||
fullAddress: lnurlAddress,
|
||||
};
|
||||
}
|
||||
|
||||
// External-chain stablecoins name their own asset + chain, so they check ahead
|
||||
// of the payment-type branches — a stablecoin send is also paymentType 'spark'.
|
||||
if (stablecoinInfo) {
|
||||
return {
|
||||
kind: 'stablecoin',
|
||||
displayName: i18next.t('screens.inAccount.confirmTxPage.stablecoinDesc', {
|
||||
asset: stablecoinInfo.asset,
|
||||
chain: stablecoinInfo.label,
|
||||
}),
|
||||
logo: stablecoinInfo.label,
|
||||
};
|
||||
}
|
||||
|
||||
// Which balance funded the send. USDB rides the same LRC20 rails as any other
|
||||
// token, so the token id is what separates "Dollar" from "Token".
|
||||
const details = transaction?.details || {};
|
||||
const isDollarBalance =
|
||||
!!details.isLRC20Payment && details.LRC20Token === USDB_TOKEN_ID;
|
||||
const isToken = !!details.isLRC20Payment && !isDollarBalance;
|
||||
const assetLabel = paymentAssetLabel({ isDollarBalance, isToken });
|
||||
|
||||
// Every rail we send over resolves to the same asset card. `destinationChain`
|
||||
// catches a bolt11 funded from the dollar balance, which payments.js records as
|
||||
// paymentType 'spark'. Anything else has nothing to name.
|
||||
const paymentType = transaction?.paymentType;
|
||||
if (
|
||||
paymentType === 'lightning' ||
|
||||
paymentType === 'bitcoin' ||
|
||||
paymentType === 'spark' ||
|
||||
details.destinationChain === 'lightning'
|
||||
) {
|
||||
return {
|
||||
...resolveAssetAvatar({ isDollarBalance, isToken }),
|
||||
displayName: assetLabel,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Renders the circular avatar for a resolved recipient: branta/contact profile
|
||||
// image, provider brand logo, mobile-money country flag, or the standard user-icon
|
||||
// fallback for plain lightning addresses.
|
||||
export function RecipientAvatar({
|
||||
resolved,
|
||||
theme,
|
||||
darkModeType,
|
||||
size = 40,
|
||||
backgroundColor,
|
||||
}) {
|
||||
const isProvider = resolved?.kind === 'provider';
|
||||
const circle = [
|
||||
styles.circle,
|
||||
{ width: size, height: size, borderRadius: size / 2 },
|
||||
isProvider ? styles.providerCircle : { backgroundColor },
|
||||
];
|
||||
|
||||
if (resolved?.kind === 'phone') {
|
||||
return (
|
||||
<View style={circle}>
|
||||
{/* The flag renders 1.6x wider than `size`, so scale it to fit the
|
||||
circle's diameter instead of getting cropped by the border radius. */}
|
||||
<CountryFlag isoCode={resolved.isoCode} size={Math.round(size / 1.6)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isProvider) {
|
||||
return (
|
||||
<View style={circle}>
|
||||
<Image
|
||||
style={styles.fill}
|
||||
source={ICONS[resolved.providerIconKey]}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (resolved?.kind === 'spark') {
|
||||
return (
|
||||
<View style={circle}>
|
||||
<Image
|
||||
style={styles.fill}
|
||||
source={ICONS[resolved.logo]}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (resolved?.kind === 'stablecoin') {
|
||||
return (
|
||||
<View style={circle}>
|
||||
<Image
|
||||
style={styles.fill}
|
||||
source={ICONS[`chain_${resolved.logo?.toLowerCase()}`]}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (resolved?.kind === 'asset') {
|
||||
const isDollar = resolved.asset === 'dollar';
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
circle,
|
||||
{
|
||||
backgroundColor:
|
||||
theme && darkModeType
|
||||
? backgroundColor
|
||||
: COLORS[isDollar ? 'dollarGreen' : 'bitcoinOrange'],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
style={styles.assetIcon}
|
||||
source={ICONS[isDollar ? 'dollarIcon' : 'bitcoinIcon']}
|
||||
contentFit="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// branta / contact / lightning fallback all go through ContactProfileImage,
|
||||
// which renders the user-icon fallback when there is no uri.
|
||||
return (
|
||||
<View style={circle}>
|
||||
<ContactProfileImage
|
||||
uri={resolved?.imageUri}
|
||||
updated={resolved?.imageUpdated}
|
||||
darkModeType={darkModeType}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
circle: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
providerCircle: {
|
||||
backgroundColor: COLORS.white,
|
||||
borderColor: COLORS.gray,
|
||||
},
|
||||
fill: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
assetIcon: {
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
},
|
||||
});
|
||||
@@ -217,6 +217,11 @@ export default function SendPaymentScreen(props) {
|
||||
? masterInfoObject?.defaultSpendToken || 'Bitcoin'
|
||||
: 'Bitcoin';
|
||||
|
||||
const selectedContactInfo = contactInfo || paymentInfo?.blitzContactInfo;
|
||||
|
||||
// Intentionally keyed off `contactInfo` only: including blitzContactInfo here
|
||||
// would swap the token selector for the BTC/USD chooser and unpin the payment
|
||||
// method for @username sends. Display is handled by the InvoiceInfo props.
|
||||
const useFullTokensDisplay =
|
||||
enabledLRC20 &&
|
||||
isSparkPayment &&
|
||||
@@ -239,7 +244,14 @@ export default function SendPaymentScreen(props) {
|
||||
? paymentInfo?.data?.maxSendable / 1000
|
||||
: 0;
|
||||
|
||||
const selectedLRC20Asset = masterTokenInfo?.tokenName || defaultToken;
|
||||
// Tokens only exist on Spark — a bolt11/on-chain/LNURL invoice can never be
|
||||
// funded with the default spend token. Before the decode lands paymentNetwork
|
||||
// is undefined, so keep the user's pick: processSparkAddress reads it to
|
||||
// decide isLRC20.
|
||||
const selectedLRC20Asset =
|
||||
!paymentInfo?.paymentNetwork || useFullTokensDisplay
|
||||
? masterTokenInfo?.tokenName || defaultToken
|
||||
: 'Bitcoin';
|
||||
const seletctedToken =
|
||||
masterTokenInfo?.details ||
|
||||
sparkInformation?.tokens?.[selectedLRC20Asset] ||
|
||||
@@ -247,6 +259,12 @@ export default function SendPaymentScreen(props) {
|
||||
const tokenDecimals = seletctedToken?.tokenMetadata?.decimals ?? 0;
|
||||
const tokenBalance = seletctedToken?.balance ?? 0;
|
||||
const isUsingLRC20 = selectedLRC20Asset?.toLowerCase() !== 'bitcoin';
|
||||
// The ticker actually shown on this screen. USDB-funded sends that weren't
|
||||
// picked through the token selector display as fiat, so the confirm screen
|
||||
// must be told the label instead of re-deriving it from the tx's token info.
|
||||
const displayTokenTicker = isUsingLRC20
|
||||
? seletctedToken?.tokenMetadata?.tokenTicker
|
||||
: '';
|
||||
|
||||
const sendingAmount = paymentInfo?.sendAmount || 0;
|
||||
const canEditAmount = paymentInfo?.canEditPayment === true;
|
||||
@@ -1301,9 +1319,10 @@ export default function SendPaymentScreen(props) {
|
||||
paymentInfo?.type === InputTypes.LNURL_PAY
|
||||
? normalizeLNURLAddress(paymentInfo?.data?.address)
|
||||
: undefined,
|
||||
blitzContactInfo: paymentInfo?.blitzContactInfo,
|
||||
blitzContactInfo: selectedContactInfo,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount,
|
||||
displayTokenTicker,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1331,9 +1350,10 @@ export default function SendPaymentScreen(props) {
|
||||
paymentInfo?.type === InputTypes.LNURL_PAY
|
||||
? normalizeLNURLAddress(paymentInfo?.data?.address)
|
||||
: undefined,
|
||||
blitzContactInfo: paymentInfo?.blitzContactInfo,
|
||||
blitzContactInfo: selectedContactInfo,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount,
|
||||
displayTokenTicker,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1373,6 +1393,8 @@ export default function SendPaymentScreen(props) {
|
||||
fiatValueConvertedSendAmount,
|
||||
paymentValidation,
|
||||
displayAmount,
|
||||
displayTokenTicker,
|
||||
selectedContactInfo,
|
||||
]);
|
||||
|
||||
const handleSelectPaymentMethod = useCallback(
|
||||
@@ -1446,15 +1468,6 @@ export default function SendPaymentScreen(props) {
|
||||
});
|
||||
}, [paymentInfo?.verificationURL, navigate, t]);
|
||||
|
||||
const sendingAsset =
|
||||
selectedLRC20Asset === 'Bitcoin'
|
||||
? !isLightningPayment &&
|
||||
!isBitcoinPayment &&
|
||||
!(isSparkPayment && receiverExpectsCurrency === 'sats')
|
||||
? t('constants.dollars_upper')
|
||||
: t('constants.bitcoin_upper')
|
||||
: seletctedToken?.tokenMetadata?.tokenTicker;
|
||||
|
||||
if (
|
||||
(!Object.keys(paymentInfo).length && !errorMessage) ||
|
||||
!sparkInformation.didConnect
|
||||
@@ -1501,9 +1514,7 @@ export default function SendPaymentScreen(props) {
|
||||
forceCurrency={primaryDisplay.forceCurrency}
|
||||
forceFiatStats={primaryDisplay.forceFiatStats}
|
||||
activeOpacity={!sendingAmount ? 0.5 : 1}
|
||||
customCurrencyCode={
|
||||
isUsingLRC20 ? seletctedToken?.tokenMetadata?.tokenTicker : ''
|
||||
}
|
||||
customCurrencyCode={displayTokenTicker}
|
||||
maxDecimals={isUsingLRC20 ? tokenDecimals : 2}
|
||||
/>
|
||||
{uiState === 'CONFIRM_PAYMENT' && !isUsingLRC20 && (
|
||||
@@ -1549,13 +1560,16 @@ export default function SendPaymentScreen(props) {
|
||||
{uiState === 'CONFIRM_PAYMENT' && (
|
||||
<InvoiceInfo
|
||||
paymentInfo={paymentInfo}
|
||||
contactInfo={contactInfo || paymentInfo?.blitzContactInfo}
|
||||
fromPage={
|
||||
fromPage || (paymentInfo?.blitzContactInfo ? 'contacts' : '')
|
||||
}
|
||||
contactInfo={selectedContactInfo}
|
||||
fromPage={fromPage || (selectedContactInfo ? 'contacts' : '')}
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
isUsingBranta={isUsingBranta}
|
||||
isDollarBalance={
|
||||
resolvedPaymentMethod === 'USD' ||
|
||||
selectedLRC20Asset === USDB_TOKEN_ID
|
||||
}
|
||||
isToken={isUsingLRC20 && selectedLRC20Asset !== USDB_TOKEN_ID}
|
||||
/>
|
||||
)}
|
||||
{uiState === 'CHOOSE_METHOD' && (
|
||||
|
||||
@@ -502,6 +502,7 @@ export default function StablecoinSendScreen() {
|
||||
transaction: pendingTx,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount: rawInput,
|
||||
stablecoinInfo: { asset, label: chainLabel },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -528,6 +529,7 @@ export default function StablecoinSendScreen() {
|
||||
error: err.message,
|
||||
lnurlAddress: undefined,
|
||||
blitzContactInfo: undefined,
|
||||
stablecoinInfo: { asset, label: chainLabel },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -553,6 +555,8 @@ export default function StablecoinSendScreen() {
|
||||
clearCountdown,
|
||||
t,
|
||||
rawInput,
|
||||
chainLabel,
|
||||
asset,
|
||||
]);
|
||||
|
||||
const handleEmoji = newDescription => {
|
||||
@@ -780,11 +784,11 @@ export default function StablecoinSendScreen() {
|
||||
content={`${truncateAddress(address)}`}
|
||||
/>
|
||||
</View>
|
||||
<ThemeIcon
|
||||
{/* <ThemeIcon
|
||||
iconName="ChevronRight"
|
||||
size={20}
|
||||
styles={styles.destinationChevron}
|
||||
/>
|
||||
/> */}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -926,20 +930,22 @@ const styles = StyleSheet.create({
|
||||
width: '80%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'center',
|
||||
padding: 12,
|
||||
borderRadius: 16,
|
||||
...CENTER,
|
||||
marginTop: 30,
|
||||
},
|
||||
destinationContent: {
|
||||
flexShrink: 1,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
destinationChevron: {
|
||||
opacity: 0.8,
|
||||
marginLeft: 8,
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
},
|
||||
receiveAmount: {
|
||||
opacity: HIDDEN_OPACITY,
|
||||
@@ -953,6 +959,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 20,
|
||||
},
|
||||
quoteValue: {
|
||||
flexShrink: 1,
|
||||
fontSize: SIZES.medium,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
|
||||
@@ -62,6 +62,30 @@ export function getPhonePaymentCountry(address) {
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
// Human-readable display for a phone-payment provider address (either LNURL or
|
||||
// POST provider). Returns the provider country iso code (for a flag) and the
|
||||
// phone number in international format, or null for non-phone addresses. The
|
||||
// local part is national for some providers (ZM -> 0977…), so we parse trying
|
||||
// the international form first, then the resolved country.
|
||||
export function getPhonePaymentDisplay(address) {
|
||||
const isoCode = getPhonePaymentCountry(address);
|
||||
if (!isoCode) return null;
|
||||
const local = address.slice(0, address.indexOf('@'));
|
||||
const attempts = [
|
||||
[local.startsWith('+') ? local : `+${local}`, undefined],
|
||||
[local, isoCode],
|
||||
];
|
||||
for (const [value, defaultCountry] of attempts) {
|
||||
try {
|
||||
const parsed = parsePhoneNumberWithError(value, defaultCountry);
|
||||
if (parsed.isValid()) {
|
||||
return { isoCode, formatted: parsed.formatInternational() };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return { isoCode, formatted: local };
|
||||
}
|
||||
|
||||
// Returns the provider lightning addresses the input is valid for, in
|
||||
// PHONE_PAYMENT_PROVIDERS order (KE before ZM). Accepts national or
|
||||
// international input. A bare national number in the overlapping 075/076/077
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet, View, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
import { CENTER, COLORS, FONT, SIZES } from '../../constants';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -20,29 +20,28 @@ import formatTokensNumber from '../../functions/lrc20/formatTokensBalance';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStatus } from '../../../context-store/appStatus';
|
||||
import DropdownMenu from '../../functions/CustomElements/dropdownMenu';
|
||||
import displayCorrectDenomination from '../../functions/displayCorrectDenomination';
|
||||
import { useGlobalContextProvider } from '../../../context-store/context';
|
||||
import { useNodeContext } from '../../../context-store/nodeContext';
|
||||
import customUUID from '../../functions/customUUID';
|
||||
import { useGlobalContactsInfo } from '../../../context-store/globalContacts';
|
||||
import { getSingleContact } from '../../../db';
|
||||
import { getCachedProfileImage } from '../../functions/cachedImage';
|
||||
import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH } from '../../constants/theme';
|
||||
import { INSET_WINDOW_WIDTH } from '../../constants/theme';
|
||||
import normalizeLNURLAddress from '../../functions/lnurl/normalizeLNURLAddress';
|
||||
import { isBlitzLNURLAddress } from '../../functions/lnurl';
|
||||
import { canonicalizePhonePaymentAddress } from '../../functions/sendBitcoin/getPhonePaymentAddress';
|
||||
import FormattedBalanceInput from '../../functions/CustomElements/formattedBalanceInput';
|
||||
import {
|
||||
RecipientAvatar,
|
||||
resolveRecipientDisplay,
|
||||
} from '../../components/admin/homeComponents/sendBitcoin/components/recipientCard';
|
||||
|
||||
const confirmTxAnimation = require('../../assets/confirmTxAnimation.json');
|
||||
const errorTxAnimation = require('../../assets/errorTxAnimation.json');
|
||||
export default function ConfirmTxPage(props) {
|
||||
const { sparkInformation } = useSparkWallet();
|
||||
const { screenDimensions } = useAppStatus();
|
||||
const { masterInfoObject } = useGlobalContextProvider();
|
||||
const { fiatStats } = useNodeContext();
|
||||
const navigate = useNavigation();
|
||||
const { showToast } = useToast();
|
||||
const { backgroundOffset, textColor } = GetThemeColors();
|
||||
const { backgroundOffset, backgroundColor, textColor } = GetThemeColors();
|
||||
const { theme, darkModeType } = useGlobalThemeContext();
|
||||
const animationRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -57,9 +56,13 @@ export default function ConfirmTxPage(props) {
|
||||
const lnurlUsername = lnurlAddress?.split('@')[0]?.toLowerCase();
|
||||
const blitzContactInfo = props.route.params?.blitzContactInfo;
|
||||
const displayAmount = props.route.params?.displayAmount;
|
||||
const stablecoinInfo = props.route.params?.stablecoinInfo;
|
||||
// The display currency the user entered/reviewed the payment in (e.g. EUR),
|
||||
// passed from the send screens so the success amount matches what they saw.
|
||||
const paymentDisplay = props.route.params?.paymentDisplay;
|
||||
// Set only when the send screen actually labelled the amount with a token
|
||||
// ticker. USDB-funded sends carry token info on the tx but were shown as fiat.
|
||||
const displayTokenTicker = props.route.params?.displayTokenTicker;
|
||||
|
||||
const didSucceed = !hasError || isLNURLAuth;
|
||||
|
||||
@@ -87,18 +90,39 @@ export default function ConfirmTxPage(props) {
|
||||
const showAddBlitzContact =
|
||||
didSucceed && !isLNURLAuth && blitzContactInfo && !isAlreadyBlitzContact;
|
||||
|
||||
const paymentNetwork = paymentInformation?.sendingUUID
|
||||
? t('screens.inAccount.expandedTxPage.contactPaymentType')
|
||||
: paymentInformation?.isGift
|
||||
? t('constants.gift')
|
||||
: transaction?.paymentType;
|
||||
// Only on-chain and cross-chain stablecoin sends actually leave the user
|
||||
// waiting — a lightning tx is written 'pending' but settles immediately.
|
||||
const showPendingMessage =
|
||||
transaction?.paymentStatus === 'pending' &&
|
||||
(paymentInformation?.isFlashnetStablecoin ||
|
||||
transaction?.paymentType === 'bitcoin');
|
||||
|
||||
const showPendingMessage = transaction?.paymentStatus === 'pending';
|
||||
const isFlashnetStablecoinPending =
|
||||
paymentInformation?.isFlashnetStablecoin &&
|
||||
transaction?.paymentStatus === 'pending';
|
||||
// Recipient "name card": show a real avatar (branta logo, LNURL provider logo,
|
||||
// contact image, or mobile-money country flag) + name for single-recipient sends.
|
||||
// All the data is already in the route params / tx details — no extra wiring.
|
||||
const recipientResolved = resolveRecipientDisplay({
|
||||
lnurlAddress,
|
||||
contactInfo: blitzContactInfo,
|
||||
brantaName: paymentInformation?.brantaMerchantName,
|
||||
brantaLogo: paymentInformation?.brantaMerchantLogo,
|
||||
transaction,
|
||||
stablecoinInfo,
|
||||
});
|
||||
const showRecipientCard = !isLNURLAuth && !!recipientResolved;
|
||||
|
||||
const paymentFee = paymentInformation?.fee;
|
||||
const statusSubtitle = isLNURLAuth
|
||||
? t('screens.inAccount.confirmTxPage.lnurlAuthSuccess')
|
||||
: !didSucceed
|
||||
? t('screens.inAccount.confirmTxPage.paymentErrorMessage')
|
||||
: showPendingMessage
|
||||
? t('screens.inAccount.confirmTxPage.sendingInProgress')
|
||||
: t('screens.inAccount.confirmTxPage.confirmMessage', {
|
||||
context:
|
||||
paymentInformation.direction?.toLowerCase() === 'outgoing'
|
||||
? 'sent'
|
||||
: 'received',
|
||||
});
|
||||
const buttonText = t('constants.done');
|
||||
|
||||
const errorMessage = hasError || t('errormessages.genericError');
|
||||
|
||||
@@ -132,102 +156,6 @@ 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: {
|
||||
...masterInfoObject,
|
||||
userBalanceDenomination: paymentDisplay?.denomination
|
||||
? paymentDisplay.denomination
|
||||
: masterInfoObject.userBalanceDenomination,
|
||||
},
|
||||
fiatStats: paymentDisplay?.paymentDisplay
|
||||
? paymentDisplay.forceFiatStats
|
||||
: fiatStats,
|
||||
forceCurrency: paymentDisplay?.forceCurrency
|
||||
? paymentDisplay.forceCurrency
|
||||
: undefined,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.paymentTableRow}>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.labelText}
|
||||
content={t('constants.type')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{ textTransform: 'capitalize' }}
|
||||
content={t('screens.inAccount.expandedTxPage.chainSwap', {
|
||||
context:
|
||||
paymentInformation.destinationChain === 'lightning'
|
||||
? 'lightning'
|
||||
: 'other',
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.route.params?.isSplitPayment && props.route.params?.isRequset) {
|
||||
return (
|
||||
<GlobalThemeView useStandardWidth={true} styles={styles.globalConatianer}>
|
||||
@@ -272,204 +200,128 @@ export default function ConfirmTxPage(props) {
|
||||
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
{!isLNURLAuth && (
|
||||
<ThemeText
|
||||
styles={{ fontSize: SIZES.large, marginBottom: 10 }}
|
||||
content={
|
||||
!didSucceed
|
||||
? t('screens.inAccount.confirmTxPage.failedToSend')
|
||||
: t('screens.inAccount.confirmTxPage.confirmMessage', {
|
||||
context:
|
||||
paymentInformation.direction?.toLowerCase() === 'outgoing'
|
||||
? 'sent'
|
||||
: 'received',
|
||||
})
|
||||
}
|
||||
<View style={styles.contentContainer}>
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
source={didSucceed ? confirmAnimation : errorAnimation}
|
||||
loop={false}
|
||||
style={{
|
||||
width: 125,
|
||||
height: 125,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(didSucceed || amount > 0) && !isLNURLAuth && (
|
||||
<View style={{ marginBottom: 10 }}>
|
||||
{displayAmount && paymentDisplay ? (
|
||||
<FormattedBalanceInput
|
||||
maxWidth={0.7}
|
||||
amountValue={displayAmount}
|
||||
inputDenomination={paymentDisplay.denomination}
|
||||
forceCurrency={paymentDisplay.forceCurrency}
|
||||
forceFiatStats={paymentDisplay.forceFiatStats}
|
||||
customCurrencyCode={displayTokenTicker}
|
||||
maxDecimals={
|
||||
displayTokenTicker ? token?.tokenMetadata?.decimals ?? 0 : 2
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormattedSatText
|
||||
styles={{
|
||||
fontSize: 45,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
neverHideBalance={true}
|
||||
balance={isLRC20Payment ? formattedTokensBalance : amount}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenMetadata?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
globalBalanceDenomination={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.denomination
|
||||
: undefined
|
||||
}
|
||||
forceCurrency={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceCurrency
|
||||
: null
|
||||
}
|
||||
forceFiatStats={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceFiatStats
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{didSucceed && !isLNURLAuth && (
|
||||
<View style={{ marginBottom: 10 }}>
|
||||
{displayAmount && paymentDisplay ? (
|
||||
<FormattedBalanceInput
|
||||
maxWidth={0.7}
|
||||
amountValue={displayAmount}
|
||||
inputDenomination={paymentDisplay.denomination}
|
||||
forceCurrency={paymentDisplay.forceCurrency}
|
||||
forceFiatStats={paymentDisplay.forceFiatStats}
|
||||
customCurrencyCode={token?.tokenMetadata?.tokenTicker}
|
||||
maxDecimals={
|
||||
isLRC20Payment ? token?.tokenMetadata?.decimals ?? 0 : 2
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormattedSatText
|
||||
styles={{
|
||||
fontSize: 45,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
neverHideBalance={true}
|
||||
balance={isLRC20Payment ? formattedTokensBalance : amount}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenMetadata?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
globalBalanceDenomination={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.denomination
|
||||
: undefined
|
||||
}
|
||||
forceCurrency={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceCurrency
|
||||
: null
|
||||
}
|
||||
forceFiatStats={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceFiatStats
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{isLNURLAuth && (
|
||||
<ThemeText
|
||||
styles={{
|
||||
fontFamily: FONT.Title_Medium,
|
||||
fontSize: SIZES.large,
|
||||
width: '95%',
|
||||
textAlign: 'center',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
content={t('screens.inAccount.confirmTxPage.walletConnected')}
|
||||
/>
|
||||
)}
|
||||
{!didSucceed && (
|
||||
<ThemeText
|
||||
styles={{
|
||||
fontFamily: FONT.Title_Medium,
|
||||
fontSize: SIZES.large,
|
||||
width: '95%',
|
||||
textAlign: 'center',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
content={t('screens.inAccount.confirmTxPage.failedToSend')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLNURLAuth && (
|
||||
<ThemeText
|
||||
styles={{
|
||||
opacity: 0.6,
|
||||
width: '95%',
|
||||
maxWidth: 300,
|
||||
textAlign: 'center',
|
||||
marginBottom: 40,
|
||||
}}
|
||||
content={t('screens.inAccount.confirmTxPage.lnurlAuthSuccess')}
|
||||
content={statusSubtitle}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ThemeText
|
||||
styles={{
|
||||
opacity: 0.6,
|
||||
width: '95%',
|
||||
maxWidth: 300,
|
||||
textAlign: 'center',
|
||||
marginBottom: 40,
|
||||
}}
|
||||
content={
|
||||
didSucceed
|
||||
? ''
|
||||
: t('screens.inAccount.confirmTxPage.paymentErrorMessage')
|
||||
}
|
||||
/>
|
||||
|
||||
{didSucceed && !isLNURLAuth && (
|
||||
<View style={styles.paymentTable}>
|
||||
<View style={styles.paymentTableRow}>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.labelText}
|
||||
content={t('constants.fee')}
|
||||
/>
|
||||
<ThemeText
|
||||
content={displayCorrectDenomination({
|
||||
amount: paymentFee,
|
||||
masterInfoObject: {
|
||||
...masterInfoObject,
|
||||
userBalanceDenomination: paymentDisplay?.denomination
|
||||
? paymentDisplay.denomination
|
||||
: masterInfoObject.userBalanceDenomination,
|
||||
},
|
||||
fiatStats: paymentDisplay?.paymentDisplay
|
||||
? paymentDisplay.forceFiatStats
|
||||
: fiatStats,
|
||||
forceCurrency: paymentDisplay?.forceCurrency
|
||||
? paymentDisplay.forceCurrency
|
||||
: undefined,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.paymentTableRow}>
|
||||
<ThemeText
|
||||
CustomNumberOfLines={1}
|
||||
styles={styles.labelText}
|
||||
content={t('constants.type')}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{ textTransform: 'capitalize' }}
|
||||
content={paymentNetwork}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{!didSucceed && !isLNURLAuth && (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: backgroundOffset,
|
||||
borderRadius: 8,
|
||||
width: '95%',
|
||||
maxWidth: 300,
|
||||
minHeight: 100,
|
||||
}}
|
||||
>
|
||||
<ScrollView contentContainerStyle={{ padding: 10 }}>
|
||||
<ThemeText content={t('errormessages.paymentError')} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{!didSucceed && !isLNURLAuth && (
|
||||
<View style={{ marginTop: 10, marginBottom: 20 }}>
|
||||
<DropdownMenu
|
||||
options={[
|
||||
{
|
||||
label: t('screens.inAccount.confirmTxPage.emailReport'),
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: t('screens.inAccount.confirmTxPage.copyReport'),
|
||||
value: 'clipboard',
|
||||
},
|
||||
{showRecipientCard && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={recipientResolved.fullAddress ? 0.6 : 1}
|
||||
disabled={!recipientResolved.fullAddress}
|
||||
onPress={() =>
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: recipientResolved.fullAddress,
|
||||
})
|
||||
}
|
||||
style={[
|
||||
styles.recipientCard,
|
||||
{ backgroundColor: backgroundOffset },
|
||||
]}
|
||||
selectedValue=""
|
||||
placeholder={t('screens.inAccount.confirmTxPage.sendReport')}
|
||||
onSelect={async item => {
|
||||
if (item.value === 'email') {
|
||||
try {
|
||||
await openComposer({
|
||||
to: 'blake@blitzwalletapp.com',
|
||||
subject: 'Payment Failed',
|
||||
body: String(errorMessage),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Email composer error:', err);
|
||||
}
|
||||
} else if (item.value === 'clipboard') {
|
||||
copyToClipboard(String(errorMessage), showToast);
|
||||
}
|
||||
}}
|
||||
showClearIcon={false}
|
||||
showVerticalArrows={false}
|
||||
translateLabelText={false}
|
||||
customButtonStyles={{
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
textStyles={{
|
||||
...CENTER,
|
||||
textDecorationLine: 'underline',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
>
|
||||
<RecipientAvatar
|
||||
resolved={recipientResolved}
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
size={30}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={styles.recipientName}
|
||||
CustomNumberOfLines={1}
|
||||
content={recipientResolved.displayName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<CustomButton
|
||||
buttonStyles={{
|
||||
width: INSET_WINDOW_WIDTH,
|
||||
@@ -491,9 +343,50 @@ export default function ConfirmTxPage(props) {
|
||||
});
|
||||
});
|
||||
}}
|
||||
textContent={t('constants.continue')}
|
||||
textContent={buttonText}
|
||||
/>
|
||||
|
||||
{!didSucceed && !isLNURLAuth && (
|
||||
<DropdownMenu
|
||||
options={[
|
||||
{
|
||||
label: t('screens.inAccount.confirmTxPage.emailReport'),
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
label: t('screens.inAccount.confirmTxPage.copyReport'),
|
||||
value: 'clipboard',
|
||||
},
|
||||
]}
|
||||
selectedValue=""
|
||||
placeholder={t('screens.inAccount.confirmTxPage.sendReport')}
|
||||
onSelect={async item => {
|
||||
if (item.value === 'email') {
|
||||
try {
|
||||
await openComposer({
|
||||
to: 'blake@blitzwalletapp.com',
|
||||
subject: 'Payment Failed',
|
||||
body: String(errorMessage),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Email composer error:', err);
|
||||
}
|
||||
} else if (item.value === 'clipboard') {
|
||||
copyToClipboard(String(errorMessage), showToast);
|
||||
}
|
||||
}}
|
||||
showClearIcon={false}
|
||||
showVerticalArrows={false}
|
||||
translateLabelText={false}
|
||||
customButtonStyles={{
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
textStyles={{
|
||||
...CENTER,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showAddContact && (
|
||||
<CustomButton
|
||||
textStyles={{ color: textColor }}
|
||||
@@ -588,6 +481,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
contentContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
buttonText: {
|
||||
fontFamily: FONT.Descriptoin_Regular,
|
||||
},
|
||||
@@ -603,17 +497,19 @@ const styles = StyleSheet.create({
|
||||
width: 300, // adjust as necessary
|
||||
height: 300, // adjust as necessary
|
||||
},
|
||||
paymentTable: {
|
||||
rowGap: 20,
|
||||
},
|
||||
paymentTableRow: {
|
||||
width: '100%',
|
||||
minWidth: 200,
|
||||
recipientCard: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
maxWidth: '90%',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 100,
|
||||
columnGap: 10,
|
||||
},
|
||||
labelText: {
|
||||
recipientName: {
|
||||
flexShrink: 1,
|
||||
marginRight: 5,
|
||||
includeFontPadding: false,
|
||||
fontSize: SIZES.smedium,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1231,7 +1231,9 @@
|
||||
"swapRatesChangedButton": "Betrag erneut eingeben",
|
||||
"feeEstimateError": "Gebühr konnte nicht geschätzt werden, bitte versuchen Sie es erneut",
|
||||
"brantaVerification": "Diese Zahlung ist von Branta verifiziert.",
|
||||
"lightningPayment": "BTC-Zahlung"
|
||||
"lightningPayment": "BTC-Zahlung",
|
||||
"dollarPayment": "Dollar-Zahlung",
|
||||
"tokenPayment": "Token-Zahlung"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Wie möchten Sie diese Zahlung senden (BTC oder USD)?"
|
||||
@@ -2140,15 +2142,18 @@
|
||||
"confirmTxPage": {
|
||||
"failedToSend": "Senden fehlgeschlagen",
|
||||
"paymentErrorMessage": "Es gab ein Problem beim Senden dieser Zahlung, bitte versuchen Sie es erneut.",
|
||||
"sendReport": "Bericht an Entwickler senden",
|
||||
"lnurlAuthSuccess": "Wallet-Authentifizierung erfolgreich! Sie sind jetzt eingeloggt.",
|
||||
"sendReport": "Problem melden",
|
||||
"lnurlAuthSuccess": "Sie sind angemeldet und können loslegen.",
|
||||
"walletConnected": "Wallet verbunden",
|
||||
"emailReport": "Per E-Mail senden",
|
||||
"copyReport": "In die Zwischenablage kopieren",
|
||||
"confirmMessage_sent": "Erfolgreich gesendet",
|
||||
"confirmMessage_received": "Erfolgreich empfangen",
|
||||
"sendingInProgress": "Ihre Zahlung wurde gesendet. Bitte warten Sie, während sie bestätigt wird.",
|
||||
"confirmMessage": "{{direction}} erfolgreich",
|
||||
"bulkSuccess": "Anfragen erfolgreich gesendet",
|
||||
"bulkPartialSuccess": "Keine Anfragen gesendet"
|
||||
"bulkPartialSuccess": "Keine Anfragen gesendet",
|
||||
"stablecoinDesc": "{{asset}} auf {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Gesendeter Betrag",
|
||||
|
||||
@@ -1231,7 +1231,9 @@
|
||||
"swapRatesChangedButton": "Re-enter Amount",
|
||||
"feeEstimateError": "Unable to estimate fee, please try again",
|
||||
"brantaVerification": "This payment has been verified by Branta",
|
||||
"lightningPayment": "Bitcoin payment"
|
||||
"lightningPayment": "Bitcoin payment",
|
||||
"dollarPayment": "Dollar payment",
|
||||
"tokenPayment": "Token payment"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "How do you want to fund your payment?"
|
||||
@@ -2141,14 +2143,17 @@
|
||||
"failedToSend": "Failed to send",
|
||||
"confirmMessage": "{{direction}} successfully",
|
||||
"paymentErrorMessage": "There was an issue sending this payment, please try again.",
|
||||
"sendReport": "Send report to developer",
|
||||
"lnurlAuthSuccess": "Wallet authentication successful! You’re now logged in.",
|
||||
"sendReport": "Report a problem",
|
||||
"lnurlAuthSuccess": "You're signed in and ready to go.",
|
||||
"emailReport": "Send via Email",
|
||||
"copyReport": "Copy to Clipboard",
|
||||
"confirmMessage_sent": "Sent successfully",
|
||||
"confirmMessage_received": "Received successfully",
|
||||
"sendingInProgress": "Your payment has been sent. Please wait while it's confirmed.",
|
||||
"bulkSuccess": "Requests sent successfully",
|
||||
"bulkPartialSuccess": "No requests sent"
|
||||
"bulkPartialSuccess": "No requests sent",
|
||||
"stablecoinDesc": "{{asset}} on {{chain}}",
|
||||
"walletConnected": "Wallet connected"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Sent amount",
|
||||
|
||||
@@ -1239,7 +1239,9 @@
|
||||
"swapRatesChangedButton": "Volver a ingresar el monto",
|
||||
"feeEstimateError": "No se pudo estimar la comisión, inténtalo de nuevo",
|
||||
"brantaVerification": "Este pago ha sido verificado por Branta",
|
||||
"lightningPayment": "Pago Bitcoin"
|
||||
"lightningPayment": "Pago Bitcoin",
|
||||
"dollarPayment": "Pago en dólares",
|
||||
"tokenPayment": "Pago con token"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Selecciona cómo deseas financiar tu pago."
|
||||
@@ -2153,15 +2155,18 @@
|
||||
"confirmTxPage": {
|
||||
"failedToSend": "Error al enviar",
|
||||
"paymentErrorMessage": "Hubo un problema al enviar este pago, por favor intenta de nuevo.",
|
||||
"sendReport": "Enviar informe al desarrollador",
|
||||
"sendReport": "Informar de un problema",
|
||||
"lnurlAuthSuccess": "Has iniciado sesión y todo está listo.",
|
||||
"walletConnected": "Billetera conectada",
|
||||
"emailReport": "Enviar por correo electrónico",
|
||||
"copyReport": "Copiar al portapapeles",
|
||||
"confirmMessage_sent": "Enviado correctamente",
|
||||
"confirmMessage_received": "Recibido correctamente",
|
||||
"confirmMessage": "{{direction}} exitosamente",
|
||||
"lnurlAuthSuccess": "¡Autenticación de billetera exitosa! Ahora estás iniciado sesión.",
|
||||
"bulkSuccess": "Solicitudes enviadas correctamente",
|
||||
"bulkPartialSuccess": "No se enviaron solicitudes"
|
||||
"bulkPartialSuccess": "No se enviaron solicitudes",
|
||||
"sendingInProgress": "Tu pago ha sido enviado. Espera mientras se confirma.",
|
||||
"stablecoinDesc": "{{asset}} en {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Cantidad enviada",
|
||||
|
||||
@@ -1239,7 +1239,9 @@
|
||||
"swapRatesChangedButton": "Ressaisir le montant",
|
||||
"feeEstimateError": "Impossible d’estimer les frais, veuillez réessayer",
|
||||
"brantaVerification": "Ce paiement a été vérifié par Branta",
|
||||
"lightningPayment": "Paiement Bitcoin"
|
||||
"lightningPayment": "Paiement Bitcoin",
|
||||
"dollarPayment": "Paiement en dollars",
|
||||
"tokenPayment": "Paiement en jetons"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Veuillez sélectionner comment vous souhaitez financer votre paiement."
|
||||
@@ -2154,14 +2156,17 @@
|
||||
"failedToSend": "Échec de l'envoi",
|
||||
"confirmMessage": "{{direction}} avec succès",
|
||||
"paymentErrorMessage": "Il y a eu un problème lors de l'envoi de ce paiement, veuillez réessayer.",
|
||||
"sendReport": "Envoyer le rapport au développeur",
|
||||
"lnurlAuthSuccess": "Authentification du portefeuille réussie ! Vous êtes maintenant connecté.",
|
||||
"sendReport": "Signaler un problème",
|
||||
"lnurlAuthSuccess": "Vous êtes connecté et prêt à continuer.",
|
||||
"walletConnected": "Portefeuille connecté",
|
||||
"emailReport": "Envoyer par courriel",
|
||||
"copyReport": "Copier dans le presse-papiers",
|
||||
"confirmMessage_sent": "Envoyé avec succès",
|
||||
"confirmMessage_received": "Reçu avec succès",
|
||||
"bulkSuccess": "Demandes envoyées avec succès",
|
||||
"bulkPartialSuccess": "Aucune demande envoyée"
|
||||
"bulkPartialSuccess": "Aucune demande envoyée",
|
||||
"sendingInProgress": "Votre paiement a été envoyé. Veuillez patienter pendant sa confirmation.",
|
||||
"stablecoinDesc": "{{asset}} sur {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Montant envoyé",
|
||||
|
||||
@@ -1239,7 +1239,9 @@
|
||||
"swapRatesChangedButton": "Reinserisci importo",
|
||||
"feeEstimateError": "Impossibile stimare la commissione, riprova",
|
||||
"brantaVerification": "Questo pagamento è stato verificato da Branta",
|
||||
"lightningPayment": "Pagamento Bitcoin"
|
||||
"lightningPayment": "Pagamento Bitcoin",
|
||||
"dollarPayment": "Pagamento in dollari",
|
||||
"tokenPayment": "Pagamento con token"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Seleziona come desideri finanziare il pagamento."
|
||||
@@ -2153,15 +2155,18 @@
|
||||
"confirmTxPage": {
|
||||
"failedToSend": "Invio fallito",
|
||||
"paymentErrorMessage": "C'è stato un problema nell'inviare questo pagamento, riprova.",
|
||||
"sendReport": "Invia report allo sviluppatore",
|
||||
"sendReport": "Segnala un problema",
|
||||
"lnurlAuthSuccess": "Hai effettuato l'accesso e sei pronto.",
|
||||
"walletConnected": "Wallet connesso",
|
||||
"emailReport": "Invia via email",
|
||||
"copyReport": "Copia negli appunti",
|
||||
"confirmMessage_sent": "Inviato con successo",
|
||||
"confirmMessage_received": "Ricevuto con successo",
|
||||
"confirmMessage": "{{direction}} con successo",
|
||||
"lnurlAuthSuccess": "Autenticazione del portafoglio successo! Ora hai effettuato l'accesso.",
|
||||
"bulkSuccess": "Richieste inviate con successo",
|
||||
"bulkPartialSuccess": "Nessuna richiesta inviata"
|
||||
"bulkPartialSuccess": "Nessuna richiesta inviata",
|
||||
"sendingInProgress": "Il tuo pagamento è stato inviato. Attendi mentre viene confermato.",
|
||||
"stablecoinDesc": "{{asset}} su {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_received": "Importo ricevuto",
|
||||
|
||||
@@ -1013,6 +1013,7 @@
|
||||
"noContactDesc": "Adicione {{username}} aos seus contatos para encontrá-lo aqui no futuro, ou continue para enviar um pagamento agora.",
|
||||
"phonePaymentTitle": "Pagar {{number}}",
|
||||
"phonePaymentDesc": "Enviando para número de celular",
|
||||
"phonePaymentDescGcash": "Enviando para número GCash",
|
||||
"mobileMoneyTitle": "Pagamentos Locais",
|
||||
"mobileMoneySubtitle": "Métodos de pagamento por país",
|
||||
"mobileMoneyCountryTitle": "Selecione um país",
|
||||
@@ -1238,7 +1239,9 @@
|
||||
"swapRatesChangedButton": "Inserir valor novamente",
|
||||
"feeEstimateError": "Não foi possível estimar a taxa, tente novamente",
|
||||
"brantaVerification": "Este pagamento foi verificado pela Branta",
|
||||
"lightningPayment": "Pagamento Bitcoin"
|
||||
"lightningPayment": "Pagamento Bitcoin",
|
||||
"dollarPayment": "Pagamento em dólares",
|
||||
"tokenPayment": "Pagamento com token"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Escolha o saldo para realizar esse pagamento"
|
||||
@@ -2152,15 +2155,18 @@
|
||||
"confirmTxPage": {
|
||||
"failedToSend": "Falha ao enviar",
|
||||
"paymentErrorMessage": "Ocorreu um problema ao enviar este pagamento. Tente novamente.",
|
||||
"sendReport": "Enviar relatório ao desenvolvedor",
|
||||
"lnurlAuthSuccess": "Autenticação da carteira realizada com sucesso! Você está conectado.",
|
||||
"sendReport": "Relatar um problema",
|
||||
"lnurlAuthSuccess": "Você está conectado e tudo está pronto.",
|
||||
"walletConnected": "Carteira conectada",
|
||||
"emailReport": "Enviar por e-mail",
|
||||
"copyReport": "Copiar para a área de transferência",
|
||||
"confirmMessage_sent": "Enviado com sucesso",
|
||||
"confirmMessage_received": "Recebido com sucesso",
|
||||
"confirmMessage": "{{direction}} com sucesso",
|
||||
"bulkSuccess": "Solicitações enviadas com sucesso",
|
||||
"bulkPartialSuccess": "Nenhuma solicitação enviada"
|
||||
"bulkPartialSuccess": "Nenhuma solicitação enviada",
|
||||
"sendingInProgress": "Seu pagamento foi enviado. Aguarde enquanto ele é confirmado.",
|
||||
"stablecoinDesc": "{{asset}} na {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Valor enviado",
|
||||
|
||||
@@ -1240,7 +1240,9 @@
|
||||
"swapRatesChangedButton": "Ввести сумму снова",
|
||||
"feeEstimateError": "Не удалось оценить комиссию, попробуйте снова",
|
||||
"brantaVerification": "Этот платёж был подтверждён Branta",
|
||||
"lightningPayment": "Платёж Bitcoin"
|
||||
"lightningPayment": "Платёж Bitcoin",
|
||||
"dollarPayment": "Платёж в долларах",
|
||||
"tokenPayment": "Платёж токенами"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Выберите источник средств."
|
||||
@@ -2155,14 +2157,17 @@
|
||||
"failedToSend": "Не удалось отправить",
|
||||
"confirmMessage": "{{direction}} успешно",
|
||||
"paymentErrorMessage": "Проблема с отправкой, попробуйте снова.",
|
||||
"sendReport": "Отправить отчет разработчику",
|
||||
"lnurlAuthSuccess": "Вход выполнен успешно!",
|
||||
"sendReport": "Сообщить о проблеме",
|
||||
"lnurlAuthSuccess": "Вы вошли в систему и готовы к работе.",
|
||||
"walletConnected": "Кошелёк подключён",
|
||||
"emailReport": "Отправить через Email",
|
||||
"copyReport": "Скопировать",
|
||||
"confirmMessage_sent": "Успешно отправлено",
|
||||
"confirmMessage_received": "Успешно получено",
|
||||
"bulkSuccess": "Запросы успешно отправлены",
|
||||
"bulkPartialSuccess": "Запросы не отправлены"
|
||||
"bulkPartialSuccess": "Запросы не отправлены",
|
||||
"sendingInProgress": "Ваш платёж отправлен. Пожалуйста, подождите, пока он будет подтверждён.",
|
||||
"stablecoinDesc": "{{asset}} в сети {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Отправлено",
|
||||
|
||||
@@ -1239,7 +1239,9 @@
|
||||
"swapRatesChangedButton": "Ange belopp igen",
|
||||
"feeEstimateError": "Kunde inte uppskatta avgiften, försök igen",
|
||||
"brantaVerification": "Denna betalning har verifierats av Branta",
|
||||
"lightningPayment": "Bitcoin-betalning"
|
||||
"lightningPayment": "Bitcoin-betalning",
|
||||
"dollarPayment": "Dollarbetalning",
|
||||
"tokenPayment": "Tokenbetalning"
|
||||
},
|
||||
"selectPaymentMethod": {
|
||||
"header": "Välj hur du vill finansiera din betalning."
|
||||
@@ -2154,14 +2156,17 @@
|
||||
"failedToSend": "Misslyckades med att skicka",
|
||||
"confirmMessage": "{{direction}} framgångsrikt",
|
||||
"paymentErrorMessage": "Det uppstod ett problem med att skicka betalningen, vänligen försök igen.",
|
||||
"sendReport": "Skicka rapporten till utvecklaren",
|
||||
"lnurlAuthSuccess": "Plånboksautentisering framgångsrik! Du är nu inloggad.",
|
||||
"sendReport": "Rapportera ett problem",
|
||||
"lnurlAuthSuccess": "Du är inloggad och redo att köra.",
|
||||
"walletConnected": "Plånbok ansluten",
|
||||
"emailReport": "Skicka via e-post",
|
||||
"copyReport": "Kopiera till Urklipp",
|
||||
"confirmMessage_sent": "Skickat framgångsrikt",
|
||||
"confirmMessage_received": "Mottaget framgångsrikt",
|
||||
"bulkSuccess": "Förfrågningar skickades",
|
||||
"bulkPartialSuccess": "Inga förfrågningar skickades"
|
||||
"bulkPartialSuccess": "Inga förfrågningar skickades",
|
||||
"sendingInProgress": "Din betalning har skickats. Vänta medan den bekräftas.",
|
||||
"stablecoinDesc": "{{asset}} på {{chain}}"
|
||||
},
|
||||
"expandedTxPage": {
|
||||
"confirmMessage_sent": "Utskickat belopp",
|
||||
|
||||
Reference in New Issue
Block a user