ui: more comprehensive Payment Success view

This commit is contained in:
Evan Kaloudis
2026-02-12 09:23:09 -05:00
parent d94abfb075
commit 10db78bcdc
14 changed files with 729 additions and 126 deletions
+21 -3
View File
@@ -33,6 +33,7 @@ interface AmountDisplayProps {
space?: boolean;
jumboText?: boolean;
defaultTextSize?: boolean;
fontSize?: number;
color?:
| 'text'
| 'success'
@@ -63,6 +64,7 @@ function AmountDisplay({
space = false,
jumboText = false,
defaultTextSize = false,
fontSize = undefined,
color = undefined,
colorOverride = undefined,
pending = false,
@@ -98,6 +100,7 @@ function AmountDisplay({
<Body
secondary
jumbo={jumboText}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -111,6 +114,7 @@ function AmountDisplay({
<Body
secondary
jumbo={jumboText}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -124,6 +128,7 @@ function AmountDisplay({
<Body
jumbo={jumboText}
defaultSize={defaultTextSize}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -133,7 +138,12 @@ function AmountDisplay({
);
const TextSpace = () => (
<Body jumbo={jumboText} color={color} colorOverride={colorOverride}>
<Body
jumbo={jumboText}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
>
{' '}
</Body>
);
@@ -156,6 +166,7 @@ function AmountDisplay({
<Body
jumbo={jumboText}
defaultSize={defaultTextSize}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -167,8 +178,9 @@ function AmountDisplay({
<View accessible={accessible}>
<Body
secondary
small={!jumboText}
small={!jumboText && !fontSize}
defaultSize={defaultTextSize}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -194,8 +206,9 @@ function AmountDisplay({
<View>
<Body
secondary
small={!jumboText}
small={!jumboText && !fontSize}
defaultSize={defaultTextSize}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
>
@@ -209,6 +222,7 @@ function AmountDisplay({
<Body
jumbo={jumboText}
defaultSize={defaultTextSize}
fontSize={fontSize}
color={color}
colorOverride={colorOverride}
accessible={accessible}
@@ -306,6 +320,7 @@ interface AmountProps {
sensitiveLength?: number;
jumboText?: boolean;
defaultTextSize?: boolean;
fontSize?: number;
credit?: boolean;
debit?: boolean;
// If credit or debit doesn't cover the use case
@@ -337,6 +352,7 @@ export default class Amount extends React.Component<AmountProps, {}> {
sensitiveLength = 4,
jumboText = false,
defaultTextSize = false,
fontSize = undefined,
credit = false,
debit = false,
toggleable = false,
@@ -395,6 +411,7 @@ export default class Amount extends React.Component<AmountProps, {}> {
negative={negative}
jumboText={jumboText}
defaultTextSize={defaultTextSize}
fontSize={fontSize}
pending={pending}
fee={fee}
fiatRatesLoading={FiatStore.loading}
@@ -466,6 +483,7 @@ export default class Amount extends React.Component<AmountProps, {}> {
negative={negative}
jumboText={jumboText}
defaultTextSize={defaultTextSize}
fontSize={fontSize}
color={textColor}
colorOverride={colorOverride}
pending={pending}
+356
View File
@@ -0,0 +1,356 @@
import * as React from 'react';
import {
Animated,
Image,
Modal,
PanResponder,
PanResponderInstance,
ScrollView,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';
import Amount from './Amount';
import KeyValue from './KeyValue';
import { Row } from './layout/Row';
import { getFeePercentage } from '../utils/AmountUtils';
import Contact from '../models/Contact';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
import { getPhoto } from '../utils/PhotoUtils';
import CaretRight from '../assets/images/SVG/Caret Right.svg';
import CaretUp from '../assets/images/SVG/Caret Up.svg';
import CloseSvg from '../assets/images/SVG/Close.svg';
interface PaymentDetailsSheetProps {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
paymentAmount?: string | number;
feeAmount?: string | number;
paymentDuration?: number | null;
contact?: Contact | null;
lightningAddress?: string;
paymentHash?: string;
paymentPreimage?: string;
enhancedPath?: any;
navigation: any;
}
export default class PaymentDetailsSheet extends React.Component<PaymentDetailsSheetProps> {
private pan: Animated.ValueXY;
private panResponder: PanResponderInstance;
constructor(props: PaymentDetailsSheetProps) {
super(props);
this.pan = new Animated.ValueXY();
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: this.pan.x, dy: this.pan.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
Animated.spring(this.pan, {
toValue: { x: 0, y: 0 },
useNativeDriver: false
}).start();
props.onOpen();
}
});
}
renderContactRow = () => {
const { contact, lightningAddress } = this.props;
if (!contact && !lightningAddress) return null;
const contactPhoto = contact?.photo ? getPhoto(contact.photo) : null;
const displayName = contact?.name || lightningAddress;
return (
<KeyValue
keyValue={localeString('views.Payment.recipient')}
value={
<Row>
{contactPhoto ? (
<Image
source={{ uri: contactPhoto }}
style={{
width: 30,
height: 30,
borderRadius: 15,
marginRight: 8
}}
/>
) : null}
<Text
style={{
fontFamily: 'PPNeueMontreal-Book',
color: themeColor('text')
}}
>
{displayName}
</Text>
</Row>
}
/>
);
};
renderPathRow = () => {
const { enhancedPath, navigation, onClose } = this.props;
const pathExists = enhancedPath?.length > 0 && enhancedPath[0][0];
if (!pathExists) return null;
return (
<TouchableOpacity
onPress={() => {
onClose();
navigation.navigate('PaymentPaths', {
enhancedPath
});
}}
>
<View style={{ marginTop: 10, marginBottom: 10 }}>
<Row justify="space-between">
<View style={{ flex: 1 }}>
<KeyValue
keyValue={
enhancedPath.length > 1
? `${localeString(
'views.Payment.paths'
)} (${enhancedPath.length})`
: localeString('views.Payment.path')
}
disableCopy
/>
</View>
<CaretRight
fill={themeColor('text')}
width="20"
height="20"
/>
</Row>
</View>
</TouchableOpacity>
);
};
renderTrigger = () => {
return (
<Animated.View
style={{
width: '100%',
transform: [{ translateY: this.pan.y }],
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 5
}}
{...this.panResponder.panHandlers}
>
<TouchableOpacity
onPress={this.props.onOpen}
accessibilityLabel={localeString('views.Payment.details')}
style={{
alignItems: 'center',
padding: 10
}}
>
<CaretUp fill={themeColor('text')} />
<Text
style={{
marginTop: 7,
textAlign: 'center',
fontFamily: 'PPNeueMontreal-Book',
color: themeColor('text')
}}
>
{localeString('views.Payment.details')}
</Text>
</TouchableOpacity>
</Animated.View>
);
};
render() {
const {
isOpen,
onClose,
paymentAmount,
feeAmount,
paymentDuration,
paymentHash,
paymentPreimage
} = this.props;
const feePercentage = getFeePercentage(feeAmount, paymentAmount);
return (
<>
<Modal
visible={isOpen}
transparent={true}
animationType="fade"
onRequestClose={onClose}
>
<TouchableWithoutFeedback onPress={onClose}>
<View
style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center'
}}
>
<TouchableWithoutFeedback>
<View
style={{
backgroundColor:
themeColor('secondary'),
borderRadius: 24,
padding: 20,
width: '90%',
maxHeight: '80%'
}}
>
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16
}}
>
<Text
style={{
fontFamily:
'PPNeueMontreal-Medium',
color: themeColor('text'),
fontSize: 20,
textAlign: 'center',
flex: 1
}}
>
{localeString(
'views.Payment.details'
)}
</Text>
<TouchableOpacity
onPress={onClose}
hitSlop={{
top: 8,
bottom: 8,
left: 8,
right: 8
}}
style={{ padding: 4 }}
>
<CloseSvg
fill={themeColor('text')}
width={16}
height={16}
/>
</TouchableOpacity>
</View>
<ScrollView>
{paymentAmount != null && (
<KeyValue
keyValue={localeString(
'views.Receive.amount'
)}
value={
<Amount
sats={paymentAmount}
debit
sensitive
toggleable
/>
}
/>
)}
{feeAmount != null && (
<KeyValue
keyValue={localeString(
'views.Payment.fee'
)}
value={
<Row>
<Amount
sats={feeAmount}
debit
sensitive
toggleable
/>
{feePercentage ? (
<Text
style={{
fontFamily:
'PPNeueMontreal-Book',
color: themeColor(
'text'
)
}}
>
{` (${feePercentage})`}
</Text>
) : null}
</Row>
}
/>
)}
{paymentDuration != null && (
<KeyValue
keyValue={localeString(
'views.Payment.settlementTime'
)}
value={`${paymentDuration.toFixed(
2
)}s`}
disableCopy
/>
)}
{this.renderContactRow()}
{paymentHash ? (
<KeyValue
keyValue={localeString(
'views.Payment.paymentHash'
)}
value={paymentHash}
sensitive
/>
) : null}
{paymentPreimage ? (
<KeyValue
keyValue={localeString(
'views.Payment.paymentPreimage'
)}
value={paymentPreimage}
sensitive
/>
) : null}
{this.renderPathRow()}
</ScrollView>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
{this.renderTrigger()}
</>
);
}
}
+5 -1
View File
@@ -10,6 +10,7 @@ export function Body({
big = false,
jumbo = false,
defaultSize = false,
fontSize: fontSizeOverride = undefined,
color = undefined,
colorOverride = undefined,
children,
@@ -22,6 +23,7 @@ export function Body({
big?: boolean;
jumbo?: boolean;
defaultSize?: boolean;
fontSize?: number;
// These should only be keys available on the theme
// TODO: enforce this with some global ThemeKey enum?
colorOverride?: string;
@@ -50,7 +52,9 @@ export function Body({
: secondary
? themeColor('secondaryText')
: themeColor('text'),
fontSize: small
fontSize: fontSizeOverride
? fontSizeOverride
: small
? 12
: big
? 20
+3
View File
@@ -689,6 +689,9 @@
"views.Payment.creationDate": "Creation Date",
"views.Payment.path": "Path",
"views.Payment.paths": "Paths",
"views.Payment.details": "Payment Details",
"views.Payment.recipient": "Recipient",
"views.Payment.settlementTime": "Settlement time",
"views.Payment.writeNote": "Write your note here",
"views.PaymentRequest.title": "Lightning Invoice",
"views.PaymentRequest.error": "Error loading invoice",
+2 -10
View File
@@ -5,6 +5,7 @@ import humanizeDuration from 'humanize-duration';
import BaseModel from './BaseModel';
import DateTimeUtils from '../utils/DateTimeUtils';
import { getFeePercentage } from '../utils/AmountUtils';
import { localeString } from '../utils/LocaleUtils';
import Bolt11Utils from '../utils/Bolt11Utils';
import Base64Utils from '../utils/Base64Utils';
@@ -305,16 +306,7 @@ export default class Payment extends BaseModel {
}
@computed public get getFeePercentage(): string {
const amount = this.getAmount;
const fee = this.getFee;
if (!fee || !amount || fee == '0') return '';
// use at most 3 decimal places and remove trailing 0s
return (
Number(new BigNumber(fee).div(amount).times(100).toFixed(3))
.toString()
.replace(/-/g, '') + '%'
);
return getFeePercentage(this.getFee, this.getAmount);
}
@computed public get enhancedPath(): any[] {
+2 -10
View File
@@ -2,6 +2,7 @@ import { computed } from 'mobx';
import BigNumber from 'bignumber.js';
import BaseModel from './BaseModel';
import { getFeePercentage } from '../utils/AmountUtils';
import DateTimeUtils from '../utils/DateTimeUtils';
import { localeString } from '../utils/LocaleUtils';
import { notesStore } from '../stores/Stores';
@@ -55,16 +56,7 @@ export default class Transaction extends BaseModel {
}
@computed public get getFeePercentage(): string {
const amount = this.getAmount;
const fee = this.getFee;
if (!fee || !amount || fee == '0' || amount == '0') return '';
// use at most 3 decimal places and remove trailing 0s
return (
Number(new BigNumber(fee).div(amount).times(100).toFixed(3))
.toString()
.replace(/-/g, '') + '%'
);
return getFeePercentage(this.getFee, this.getAmount);
}
@computed public get getTimestamp(): string | number {
+3
View File
@@ -184,6 +184,7 @@ export default class CashuStore {
@observable public noteKey?: string;
@observable public paymentStartTime?: number;
@observable public paymentDuration?: number;
@observable public paymentFee?: number;
@observable public mintRecommendations?: MintRecommendation[];
@observable public trustedMintRecommendations?: MintRecommendation[];
@@ -266,6 +267,7 @@ export default class CashuStore {
this.paymentErrorMsg = undefined;
this.paymentStartTime = undefined;
this.paymentDuration = undefined;
this.paymentFee = undefined;
};
getLndDir = () => {
@@ -2159,6 +2161,7 @@ export default class CashuStore {
if (!isDonationPayment) {
this.paymentPreimage = paymentPreimage;
this.paymentFee = realFee;
}
if (this.paymentStartTime) {
+5 -1
View File
@@ -52,6 +52,7 @@ export default class LnurlPayStore {
zaplockerNpub: string | undefined;
relays: Array<string> | undefined;
paymentRequest: string | undefined;
lightningAddress: string | undefined;
constructor(settingsStore: SettingsStore, nodeInfoStore: NodeInfoStore) {
this.settingsStore = settingsStore;
@@ -69,6 +70,7 @@ export default class LnurlPayStore {
this.zaplockerNpub = undefined;
this.relays = undefined;
this.paymentRequest = undefined;
this.lightningAddress = undefined;
};
public load = async (paymentHash: string): Promise<LnurlPayTransaction> => {
@@ -97,7 +99,8 @@ export default class LnurlPayStore {
user_pubkey?: string,
relays?: Array<string>,
relays_sig?: string,
pr?: string
pr?: string,
lightningAddress?: string
) => {
this.reset();
const now = new Date().getTime();
@@ -125,6 +128,7 @@ export default class LnurlPayStore {
this.domain = domain;
if (pr) this.paymentRequest = pr;
if (lightningAddress) this.lightningAddress = lightningAddress;
// Zaplocker
if (user_pubkey) {
+53
View File
@@ -4,6 +4,7 @@ import {
getUnformattedAmount,
getAmountFromSats,
getFormattedAmount,
getFeePercentage,
getSatAmount
} from './AmountUtils';
import { settingsStore, fiatStore, unitsStore } from '../stores/Stores';
@@ -995,4 +996,56 @@ describe('AmountUtils', () => {
});
});
});
describe('getFeePercentage', () => {
it('calculates a simple percentage', () => {
expect(getFeePercentage(10, 1000)).toBe('1%');
});
it('calculates a fractional percentage', () => {
expect(getFeePercentage(15, 1000)).toBe('1.5%');
});
it('caps at 3 decimal places and removes trailing zeros', () => {
expect(getFeePercentage(1, 3000)).toBe('0.033%');
});
it('handles string inputs', () => {
expect(getFeePercentage('50', '1000')).toBe('5%');
});
it('returns empty string when fee is 0', () => {
expect(getFeePercentage(0, 1000)).toBe('');
expect(getFeePercentage('0', 1000)).toBe('');
});
it('returns empty string when amount is 0', () => {
expect(getFeePercentage(10, 0)).toBe('');
expect(getFeePercentage(10, '0')).toBe('');
});
it('returns empty string when fee is undefined', () => {
expect(getFeePercentage(undefined, 1000)).toBe('');
});
it('returns empty string when amount is undefined', () => {
expect(getFeePercentage(10, undefined)).toBe('');
});
it('handles negative fee by stripping the minus sign', () => {
expect(getFeePercentage(-10, 1000)).toBe('1%');
});
it('handles 100% fee', () => {
expect(getFeePercentage(1000, 1000)).toBe('100%');
});
it('handles fee larger than amount', () => {
expect(getFeePercentage(1500, 1000)).toBe('150%');
});
it('handles very small percentages', () => {
expect(getFeePercentage(1, 1000000)).toBe('0%');
});
});
});
+21
View File
@@ -311,6 +311,27 @@ export function getFormattedAmount(
}
}
/**
* Calculates the fee as a percentage of the payment amount.
* Returns a string like "1.5%" or empty string if not applicable.
*
* @param fee - The fee amount (string or number)
* @param amount - The payment amount (string or number)
* @returns Formatted percentage string (e.g., "1.5%") or empty string
*/
export function getFeePercentage(
fee: string | number | undefined,
amount: string | number | undefined
): string {
if (!fee || !amount || fee == '0' || amount == '0') return '';
return (
Number(new BigNumber(fee).div(amount).times(100).toFixed(3))
.toString()
.replace(/-/g, '') + '%'
);
}
/**
* Converts satoshis to millisatoshis
* @param sats - Amount in satoshis
+17 -1
View File
@@ -2,6 +2,8 @@ import { v4 as uuidv4 } from 'uuid';
import { nip19 } from 'nostr-tools';
import RNFS from 'react-native-fs';
import Contact from '../models/Contact';
const transformContactData = async (contact: any) => {
try {
const name = contact?.display_name || contact?.name || '';
@@ -78,4 +80,18 @@ const transformContactData = async (contact: any) => {
}
};
export default { transformContactData };
const findContactByLightningAddress = (
lightningAddress: string | undefined,
contacts: Contact[] | undefined
): Contact | null => {
if (!lightningAddress || !contacts?.length) return null;
const normalized = lightningAddress.toLowerCase();
const found = contacts.find((c: any) =>
c.lnAddress?.some(
(addr: string) => addr && addr.toLowerCase() === normalized
)
);
return found ? new Contact(found) : null;
};
export default { transformContactData, findContactByLightningAddress };
+115 -29
View File
@@ -23,10 +23,12 @@ import SuccessAnimation from '../../components/SuccessAnimation';
import { Row } from '../../components/layout/Row';
import CashuStore from '../../stores/CashuStore';
import ContactStore from '../../stores/ContactStore';
import LnurlPayStore from '../../stores/LnurlPayStore';
import SettingsStore from '../../stores/SettingsStore';
import NodeInfoStore from '../../stores/NodeInfoStore';
import ContactUtils from '../../utils/ContactUtils';
import { localeString } from '../../utils/LocaleUtils';
import { themeColor } from '../../utils/ThemeUtils';
import UrlUtils from '../../utils/UrlUtils';
@@ -44,10 +46,12 @@ import Amount from '../../components/Amount';
import ModalBox from '../../components/ModalBox';
import LoadingIndicator from '../../components/LoadingIndicator';
import Header from '../../components/Header';
import PaymentDetailsSheet from '../../components/PaymentDetailsSheet';
interface CashuSendingLightningProps {
navigation: StackNavigationProp<any, any>;
CashuStore: CashuStore;
ContactStore: ContactStore;
LnurlPayStore: LnurlPayStore;
SettingsStore: SettingsStore;
NodeInfoStore: NodeInfoStore;
@@ -71,9 +75,10 @@ interface CashuSendingLightningState {
amountDonated: number | null;
donationIsPaid: boolean;
showZaplockerWarning: boolean;
showPaymentDetails: boolean;
}
@inject('CashuStore', 'LnurlPayStore', 'NodeInfoStore')
@inject('CashuStore', 'ContactStore', 'LnurlPayStore', 'NodeInfoStore')
@observer
export default class CashuSendingLightning extends React.Component<
CashuSendingLightningProps,
@@ -93,7 +98,8 @@ export default class CashuSendingLightning extends React.Component<
wasSuccessful: false,
paymentType: 'main',
donationIsPaid: false,
showZaplockerWarning: false
showZaplockerWarning: false,
showPaymentDetails: false
};
}
@@ -422,7 +428,8 @@ export default class CashuSendingLightning extends React.Component<
}
render() {
const { CashuStore, LnurlPayStore, navigation } = this.props;
const { CashuStore, ContactStore, LnurlPayStore, navigation } =
this.props;
const {
loading,
paymentError,
@@ -431,14 +438,28 @@ export default class CashuSendingLightning extends React.Component<
paymentPreimage,
paymentErrorMsg,
noteKey,
paymentDuration
paymentDuration,
paymentFee
} = CashuStore;
const payment_hash = payReq && payReq.payment_hash;
const { storedNotes, donationHandled, paymentType, payingDonation } =
this.state;
const paymentAmount = payReq?.getRequestAmount;
const {
storedNotes,
donationHandled,
paymentType,
payingDonation,
showPaymentDetails
} = this.state;
const lightningAddress = LnurlPayStore.lightningAddress;
const matchedContact = ContactUtils.findContactByLightningAddress(
lightningAddress,
ContactStore?.contacts
);
const success = this.successfullySent(CashuStore);
const windowSize = Dimensions.get('window');
const amountFontSize = windowSize.width * windowSize.scale * 0.013;
return (
<Screen>
@@ -555,6 +576,66 @@ export default class CashuSendingLightning extends React.Component<
'views.SendingLightning.success'
)}
</Text>
{paymentAmount != null && (
<Row
style={{
marginTop: 10,
alignItems: 'baseline'
}}
>
<Amount
sats={paymentAmount}
sensitive
toggleable
fontSize={amountFontSize}
/>
{paymentFee != null && (
<Row
style={{
alignItems:
'baseline'
}}
>
<Text
style={{
color: themeColor(
'secondaryText'
),
fontFamily:
'PPNeueMontreal-Book',
fontSize:
amountFontSize
}}
>
{' (+'}
</Text>
<Amount
sats={paymentFee}
sensitive
toggleable
fontSize={
amountFontSize
}
/>
<Text
style={{
color: themeColor(
'secondaryText'
),
fontFamily:
'PPNeueMontreal-Book',
fontSize:
amountFontSize
}}
>
{` ${localeString(
'views.Payment.fee'
).toLowerCase()})`}
</Text>
</Row>
)}
</Row>
)}
{paymentDuration !== undefined && (
<Text
style={{
@@ -638,30 +719,37 @@ export default class CashuSendingLightning extends React.Component<
/>
</View>
)}
{!!paymentPreimage &&
!paymentError &&
!paymentErrorMsg && (
<View style={{ width: '90%' }}>
<CopyBox
heading={localeString(
'views.Payment.paymentPreimage'
)}
headingCopied={`${localeString(
'views.Payment.paymentPreimage'
)} ${localeString(
'components.ExternalLinkModal.copied'
)}`}
theme="dark"
URL={paymentPreimage}
/>
</View>
)}
</View>
{!!success && !paymentError && !paymentErrorMsg && (
<PaymentDetailsSheet
isOpen={showPaymentDetails}
onOpen={() =>
this.setState({
showPaymentDetails: true
})
}
onClose={() =>
this.setState({
showPaymentDetails: false
})
}
paymentAmount={paymentAmount}
feeAmount={paymentFee}
paymentDuration={paymentDuration}
contact={matchedContact}
lightningAddress={lightningAddress}
paymentHash={payment_hash}
paymentPreimage={paymentPreimage}
navigation={navigation}
/>
)}
<Row
align="flex-end"
style={{
bottom: 25,
marginTop: 10,
marginBottom: 15,
alignSelf: 'center'
}}
>
@@ -684,8 +772,7 @@ export default class CashuSendingLightning extends React.Component<
secondary
buttonStyle={{ height: 40, width: '100%' }}
containerStyle={{
maxWidth: '100%',
margin: 10
maxWidth: '100%'
}}
/>
)}
@@ -799,7 +886,6 @@ const styles = StyleSheet.create({
buttons: {
width: '100%',
justifyContent: 'space-between',
gap: 15,
bottom: 15
gap: 15
}
});
+5 -3
View File
@@ -193,7 +193,7 @@ export default class LnurlPay extends React.Component<
const { navigation, CashuStore, InvoicesStore, LnurlPayStore, route } =
this.props;
const { domain, comment } = this.state;
const { domain, comment, lightningAddress } = this.state;
const ecash = route.params?.ecash;
const lnurl = route.params?.lnurlParams;
const u = url.parse(lnurl.callback);
@@ -291,7 +291,8 @@ export default class LnurlPay extends React.Component<
user_pubkey,
relays,
relays_sig,
pr
pr,
lightningAddress
);
navigation.navigate('ChoosePaymentMethod', {
@@ -341,7 +342,8 @@ export default class LnurlPay extends React.Component<
user_pubkey,
relays,
relays_sig,
pr
pr,
lightningAddress
);
navigation.navigate('PaymentRequest');
});
+121 -68
View File
@@ -27,6 +27,7 @@ import Amount from '../components/Amount';
import ModalBox from '../components/ModalBox';
import BalanceStore from '../stores/BalanceStore';
import ContactStore from '../stores/ContactStore';
import LnurlPayStore from '../stores/LnurlPayStore';
import PaymentsStore from '../stores/PaymentsStore';
import SettingsStore from '../stores/SettingsStore';
@@ -35,6 +36,7 @@ import NodeInfoStore from '../stores/NodeInfoStore';
import Base64Utils from '../utils/Base64Utils';
import BackendUtils from '../utils/BackendUtils';
import ContactUtils from '../utils/ContactUtils';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
@@ -46,11 +48,14 @@ import Wordmark from '../assets/images/SVG/wordmark-black.svg';
import Gift from '../assets/images/SVG/gift.svg';
import CopyBox from '../components/CopyBox';
import LoadingIndicator from '../components/LoadingIndicator';
import BigNumber from 'bignumber.js';
import PaymentDetailsSheet from '../components/PaymentDetailsSheet';
import { getFeePercentage } from '../utils/AmountUtils';
interface SendingLightningProps {
navigation: StackNavigationProp<any, any>;
BalanceStore: BalanceStore;
ContactStore: ContactStore;
LnurlPayStore: LnurlPayStore;
PaymentsStore: PaymentsStore;
SettingsStore: SettingsStore;
@@ -79,10 +84,12 @@ interface SendingLightningState {
donationPathExists: boolean;
donationFee: string;
donationFeePercentage: string;
showPaymentDetails: boolean;
}
@inject(
'BalanceStore',
'ContactStore',
'LnurlPayStore',
'PaymentsStore',
'SettingsStore',
@@ -113,7 +120,8 @@ export default class SendingLightning extends React.Component<
donationEnhancedPath: null,
donationPathExists: false,
donationFee: '',
donationFeePercentage: ''
donationFeePercentage: '',
showPaymentDetails: false
};
}
@@ -239,15 +247,10 @@ export default class SendingLightning extends React.Component<
donationFee = (Number(payment.fee_msat) / 1000).toString();
}
const donationFeePercentage =
Number(
new BigNumber(donationFee)
.div(amountDonated)
.times(100)
.toFixed(3)
)
.toString()
.replace(/-/g, '') + '%';
const donationFeePercentage = getFeePercentage(
donationFee,
amountDonated
);
let donationEnhancedPath = null;
let donationPathExists = false;
@@ -560,16 +563,21 @@ export default class SendingLightning extends React.Component<
}
render() {
const { TransactionsStore, SettingsStore, LnurlPayStore, navigation } =
this.props;
const {
TransactionsStore,
ContactStore,
SettingsStore,
LnurlPayStore,
navigation
} = this.props;
const {
loading,
error,
error_msg,
payment_hash,
payment_preimage,
payment_fee,
payment_error,
isIncomplete,
noteKey,
paymentDuration
} = TransactionsStore;
@@ -579,17 +587,24 @@ export default class SendingLightning extends React.Component<
currentPayment,
donationHandled,
paymentType,
payingDonation
payingDonation,
showPaymentDetails
} = this.state;
const enhancedPath = currentPayment?.enhancedPath;
const paymentAmount = currentPayment?.getAmount;
const feeAmount = payment_fee || currentPayment?.getFee;
const paymentPathExists =
enhancedPath?.length > 0 && enhancedPath[0][0];
const lightningAddress = LnurlPayStore.lightningAddress;
const matchedContact = ContactUtils.findContactByLightningAddress(
lightningAddress,
ContactStore?.contacts
);
const success = this.successfullySent(TransactionsStore);
const inTransit = this.inTransit(TransactionsStore);
const windowSize = Dimensions.get('window');
const amountFontSize = windowSize.width * windowSize.scale * 0.013;
const stack = getRouteStack();
const isSwap = stack.filter((route) => route.name === 'SwapDetails')[0];
@@ -689,6 +704,66 @@ export default class SendingLightning extends React.Component<
'views.SendingLightning.success'
)}
</Text>
{paymentAmount != null && (
<Row
style={{
marginTop: 10,
alignItems: 'baseline'
}}
>
<Amount
sats={paymentAmount}
sensitive
toggleable
fontSize={amountFontSize}
/>
{feeAmount != null && (
<Row
style={{
alignItems:
'baseline'
}}
>
<Text
style={{
color: themeColor(
'secondaryText'
),
fontFamily:
'PPNeueMontreal-Book',
fontSize:
amountFontSize
}}
>
{' (+'}
</Text>
<Amount
sats={feeAmount}
sensitive
toggleable
fontSize={
amountFontSize
}
/>
<Text
style={{
color: themeColor(
'secondaryText'
),
fontFamily:
'PPNeueMontreal-Book',
fontSize:
amountFontSize
}}
>
{` ${localeString(
'views.Payment.fee'
).toLowerCase()})`}
</Text>
</Row>
)}
</Row>
)}
{paymentDuration !== null && (
<Text
style={{
@@ -840,59 +915,41 @@ export default class SendingLightning extends React.Component<
/>
</View>
)}
{!!payment_preimage &&
!isIncomplete &&
!error &&
!payment_error && (
<View style={{ width: '90%' }}>
<CopyBox
heading={localeString(
'views.Payment.paymentPreimage'
)}
headingCopied={`${localeString(
'views.Payment.paymentPreimage'
)} ${localeString(
'components.ExternalLinkModal.copied'
)}`}
theme="dark"
URL={payment_preimage}
/>
</View>
)}
</View>
{!!success && !error && !payment_error && (
<PaymentDetailsSheet
isOpen={showPaymentDetails}
onOpen={() =>
this.setState({
showPaymentDetails: true
})
}
onClose={() =>
this.setState({
showPaymentDetails: false
})
}
paymentAmount={paymentAmount}
feeAmount={feeAmount}
paymentDuration={paymentDuration}
contact={matchedContact}
lightningAddress={lightningAddress}
paymentHash={payment_hash}
paymentPreimage={payment_preimage || undefined}
enhancedPath={enhancedPath}
navigation={navigation}
/>
)}
<Row
align="flex-end"
style={{
marginBottom: 5,
bottom: 25,
marginTop: 10,
marginBottom: 15,
alignSelf: 'center'
}}
>
{paymentPathExists && (
<Button
title={`${localeString(
'views.Payment.title'
)} ${
enhancedPath?.length > 1
? `${localeString(
'views.Payment.paths'
)} (${enhancedPath.length})`
: localeString('views.Payment.path')
} `}
onPress={() =>
navigation.navigate('PaymentPaths', {
enhancedPath
})
}
secondary
buttonStyle={{ height: 40, width: '100%' }}
containerStyle={{
maxWidth: '45%',
paddingRight: 5
}}
/>
)}
{noteKey && !error && !payment_error && (
<Button
title={
@@ -912,10 +969,7 @@ export default class SendingLightning extends React.Component<
secondary
buttonStyle={{ height: 40, width: '100%' }}
containerStyle={{
maxWidth: paymentPathExists
? '45%'
: '100%',
paddingLeft: paymentPathExists ? 5 : 0
maxWidth: '100%'
}}
/>
)}
@@ -1081,7 +1135,6 @@ const styles = StyleSheet.create({
buttons: {
width: '100%',
justifyContent: 'space-between',
gap: 15,
bottom: 15
gap: 15
}
});