Merge pull request #3657 from kaloudis/ui-lightning-address-meta
ui: add Lightning address metadata to LNURL-pay flows
This commit is contained in:
@@ -286,7 +286,8 @@ export default class LightningSwipeableRow extends Component<
|
||||
lnurlParams: response,
|
||||
ecash:
|
||||
BackendUtils.supportsCashuWallet() &&
|
||||
settings?.ecash?.enableCashu
|
||||
settings?.ecash?.enableCashu,
|
||||
lightningAddress
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
@@ -305,7 +306,8 @@ export default class LightningSwipeableRow extends Component<
|
||||
lnurlParams: data,
|
||||
ecash:
|
||||
BackendUtils.supportsCashuWallet() &&
|
||||
settings?.ecash?.enableCashu
|
||||
settings?.ecash?.enableCashu,
|
||||
lightningAddress
|
||||
});
|
||||
} else {
|
||||
throw new Error(error);
|
||||
|
||||
@@ -1112,7 +1112,9 @@ describe('handleAnything', () => {
|
||||
{
|
||||
lnurlParams: { callback: 'https://cryptoqr.net/callback' },
|
||||
satAmount: undefined,
|
||||
ecash: false
|
||||
ecash: false,
|
||||
lightningAddress:
|
||||
'https%3A%2F%2Fzapper.com%2Fpayment%2F12345@cryptoqr.net'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -594,7 +594,8 @@ const handleAnything = async (
|
||||
{
|
||||
lnurlParams: response,
|
||||
satAmount: setAmount,
|
||||
ecash
|
||||
ecash,
|
||||
lightningAddress: value
|
||||
}
|
||||
];
|
||||
})
|
||||
@@ -655,7 +656,8 @@ const handleAnything = async (
|
||||
{
|
||||
lnurlParams: lnurlpData,
|
||||
satAmount: setAmount,
|
||||
ecash
|
||||
ecash,
|
||||
lightningAddress: value
|
||||
}
|
||||
];
|
||||
} else {
|
||||
|
||||
+130
-23
@@ -1,7 +1,7 @@
|
||||
import url from 'url';
|
||||
import * as React from 'react';
|
||||
import ReactNativeBlobUtil from 'react-native-blob-util';
|
||||
import { Alert, StyleSheet, Text, View } from 'react-native';
|
||||
import { Alert, Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import querystring from 'querystring-es3';
|
||||
import { Route } from '@react-navigation/native';
|
||||
@@ -17,10 +17,13 @@ import { Row } from '../..//components/layout/Row';
|
||||
import LoadingIndicator from '../../components/LoadingIndicator';
|
||||
|
||||
import CashuStore from '../../stores/CashuStore';
|
||||
import ContactStore from '../../stores/ContactStore';
|
||||
import InvoicesStore from '../../stores/InvoicesStore';
|
||||
import LnurlPayStore from '../../stores/LnurlPayStore';
|
||||
import UnitsStore from '../../stores/UnitsStore';
|
||||
|
||||
import Contact from '../../models/Contact';
|
||||
|
||||
import LnurlPayMetadata from './Metadata';
|
||||
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
@@ -31,12 +34,19 @@ import { ScrollView } from 'react-native-gesture-handler';
|
||||
interface LnurlPayProps {
|
||||
navigation: StackNavigationProp<any, any>;
|
||||
CashuStore: CashuStore;
|
||||
ContactStore: ContactStore;
|
||||
InvoicesStore: InvoicesStore;
|
||||
LnurlPayStore: LnurlPayStore;
|
||||
UnitsStore: UnitsStore;
|
||||
route: Route<
|
||||
'LnurlPay',
|
||||
{ lnurlParams: any; amount: any; satAmount: any; ecash: boolean }
|
||||
{
|
||||
lnurlParams: any;
|
||||
amount: any;
|
||||
satAmount: any;
|
||||
ecash: boolean;
|
||||
lightningAddress?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -44,11 +54,19 @@ interface LnurlPayState {
|
||||
amount: string;
|
||||
satAmount: string | number;
|
||||
domain: string;
|
||||
lightningAddress: string;
|
||||
matchedContact: Contact | null;
|
||||
comment: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@inject('CashuStore', 'InvoicesStore', 'LnurlPayStore', 'UnitsStore')
|
||||
@inject(
|
||||
'CashuStore',
|
||||
'ContactStore',
|
||||
'InvoicesStore',
|
||||
'LnurlPayStore',
|
||||
'UnitsStore'
|
||||
)
|
||||
@observer
|
||||
export default class LnurlPay extends React.Component<
|
||||
LnurlPayProps,
|
||||
@@ -67,6 +85,8 @@ export default class LnurlPay extends React.Component<
|
||||
amount: '',
|
||||
satAmount: '',
|
||||
domain: '',
|
||||
lightningAddress: '',
|
||||
matchedContact: null,
|
||||
comment: '',
|
||||
loading: false
|
||||
};
|
||||
@@ -98,9 +118,14 @@ export default class LnurlPay extends React.Component<
|
||||
}
|
||||
|
||||
stateFromProps(props: LnurlPayProps) {
|
||||
const { route, UnitsStore } = props;
|
||||
const { route, UnitsStore, ContactStore } = props;
|
||||
const { resetUnits, units } = UnitsStore;
|
||||
const { lnurlParams: lnurl, amount, satAmount } = route.params ?? {};
|
||||
const {
|
||||
lnurlParams: lnurl,
|
||||
amount,
|
||||
satAmount,
|
||||
lightningAddress
|
||||
} = route.params ?? {};
|
||||
|
||||
// if requested amount is fixed,
|
||||
// convert units to sats so conversion rate doesn't make things unpayable
|
||||
@@ -138,10 +163,27 @@ export default class LnurlPay extends React.Component<
|
||||
finalSatAmount = minSendableSats;
|
||||
}
|
||||
|
||||
// Find matching contact by Lightning Address
|
||||
let matchedContact: Contact | null = null;
|
||||
if (lightningAddress && ContactStore?.contacts) {
|
||||
const normalizedAddress = lightningAddress.toLowerCase();
|
||||
const found = ContactStore.contacts.find((contact: Contact) =>
|
||||
contact.lnAddress?.some(
|
||||
(addr: string) =>
|
||||
addr && addr.toLowerCase() === normalizedAddress
|
||||
)
|
||||
);
|
||||
if (found) {
|
||||
matchedContact = new Contact(found);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
amount: finalAmount,
|
||||
satAmount: finalSatAmount,
|
||||
domain: lnurl.domain,
|
||||
lightningAddress: lightningAddress || '',
|
||||
matchedContact,
|
||||
comment: ''
|
||||
};
|
||||
}
|
||||
@@ -309,10 +351,40 @@ export default class LnurlPay extends React.Component<
|
||||
|
||||
render() {
|
||||
const { navigation, route } = this.props;
|
||||
const { amount, satAmount, domain, comment, loading } = this.state;
|
||||
const {
|
||||
amount,
|
||||
satAmount,
|
||||
domain,
|
||||
lightningAddress,
|
||||
matchedContact,
|
||||
comment,
|
||||
loading
|
||||
} = this.state;
|
||||
|
||||
const lnurl = route.params?.lnurlParams;
|
||||
|
||||
// Extract image from LNURL metadata
|
||||
let metadataImage: string | null = null;
|
||||
try {
|
||||
const keypairs: Array<[string, string]> = JSON.parse(
|
||||
lnurl.metadata
|
||||
);
|
||||
metadataImage =
|
||||
keypairs
|
||||
.filter(([typ]) => typ.startsWith('image/'))
|
||||
.map(([typ, content]) => `data:${typ},${content}`)[0] ||
|
||||
null;
|
||||
} catch (err) {
|
||||
console.log('Error parsing LNURL metadata:', err);
|
||||
}
|
||||
|
||||
// Priority: Contact photo > LNURL metadata image
|
||||
const displayImage = matchedContact?.photo
|
||||
? matchedContact.getPhoto
|
||||
: metadataImage;
|
||||
const displayName = matchedContact?.name;
|
||||
const displayAddress = lightningAddress || domain;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header
|
||||
@@ -337,19 +409,40 @@ export default class LnurlPay extends React.Component<
|
||||
navigation={navigation}
|
||||
/>
|
||||
<ScrollView keyboardShouldPersistTaps="handled">
|
||||
{domain && (
|
||||
<View style={styles.content}>
|
||||
{/* Lightning Address Header */}
|
||||
{displayAddress && (
|
||||
<View style={styles.headerSection}>
|
||||
{displayImage && (
|
||||
<Image
|
||||
source={{ uri: displayImage }}
|
||||
style={styles.profileImage}
|
||||
/>
|
||||
)}
|
||||
{displayName && (
|
||||
<Text
|
||||
style={[
|
||||
styles.nameText,
|
||||
{ color: themeColor('text') }
|
||||
]}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
...styles.text,
|
||||
color: themeColor('secondaryText'),
|
||||
padding: 20,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 22
|
||||
}}
|
||||
style={[
|
||||
styles.addressText,
|
||||
{ color: themeColor('text') }
|
||||
]}
|
||||
>
|
||||
{domain}
|
||||
{displayAddress}
|
||||
</Text>
|
||||
<View style={{ marginTop: 8 }}>
|
||||
<LnurlPayMetadata
|
||||
metadata={lnurl.metadata}
|
||||
showArrow={false}
|
||||
hideImage={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -484,12 +577,6 @@ export default class LnurlPay extends React.Component<
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.metadata}>
|
||||
<LnurlPayMetadata
|
||||
metadata={lnurl.metadata}
|
||||
showArrow={false}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
@@ -502,5 +589,25 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: { paddingHorizontal: 20 },
|
||||
button: { paddingVertical: 15 },
|
||||
metadata: { padding: 20 }
|
||||
headerSection: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
profileImage: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
marginBottom: 15
|
||||
},
|
||||
nameText: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
},
|
||||
addressText: {
|
||||
fontSize: 18,
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}
|
||||
});
|
||||
|
||||
+38
-38
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Text, Image, View, ScrollView } from 'react-native';
|
||||
import { Text, Image, View } from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
@@ -7,11 +7,12 @@ import { themeColor } from '../../utils/ThemeUtils';
|
||||
interface LnurlPayMetadataProps {
|
||||
metadata: string;
|
||||
showArrow?: boolean;
|
||||
hideImage?: boolean;
|
||||
}
|
||||
|
||||
export default class LnurlPayMetadata extends React.Component<LnurlPayMetadataProps> {
|
||||
render() {
|
||||
const { metadata, showArrow = true } = this.props;
|
||||
const { metadata, showArrow = true, hideImage = false } = this.props;
|
||||
|
||||
let keypairs: Array<Array<string>>;
|
||||
try {
|
||||
@@ -28,51 +29,50 @@ export default class LnurlPayMetadata extends React.Component<LnurlPayMetadataPr
|
||||
.map(([typ, content]: any) => `data:${typ},${content}`)[0];
|
||||
|
||||
return (
|
||||
<ScrollView keyboardShouldPersistTaps="handled">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{image && !hideImage ? (
|
||||
<Image
|
||||
style={{
|
||||
width: 90,
|
||||
height: 90,
|
||||
marginBottom: 10
|
||||
}}
|
||||
source={{ uri: image }}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{image ? (
|
||||
<Image
|
||||
style={{
|
||||
width: 90,
|
||||
height: 90,
|
||||
marginBottom: 10
|
||||
}}
|
||||
source={{ uri: image }}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
<Text
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
color: showArrow
|
||||
? themeColor('highlight')
|
||||
: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: showArrow
|
||||
? themeColor('highlight')
|
||||
: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{showArrow && (
|
||||
<Icon
|
||||
name="keyboard-arrow-down"
|
||||
type="material"
|
||||
size={15}
|
||||
color={themeColor('highlight')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{text}
|
||||
</Text>
|
||||
{showArrow && (
|
||||
<Icon
|
||||
name="keyboard-arrow-down"
|
||||
type="material"
|
||||
size={15}
|
||||
color={themeColor('highlight')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user