* removing fetch requests when app is in background

* adding transaction capabilities to nwc

* style fixes + slider size bug fixes

* fixed spelling bug

* switching nwc to main wallet to lnurl so we can block navigation

* fixed "noster" spelling mistake

* fixed adding custom amount bug

* adding action notifications

* fixed eslint

* adding nostr settings homepage

* fixing safearea view since its a standalone page now

* adding nip5 verification

* remove brackets

* converted npub to hex and added preview to address

* chaning nostr connect to nostr wallet connect

* moving background register outside of use effect

* bump build version

* bump version

* fixing dropdown placement differnces

* using firebase messaging for android and expo for ios

* fixed dark mode style bug

* publish 13194 event to relay

* fixed error message page on long erros

* adding lookup invoice nwc event

* adding alrady paid invoice flag

* removing sparkID from invoice return

* added customizable error screen height prop

* improve sendpayment by blocking incoming events
This commit is contained in:
Blake Kaufman
2025-07-30 07:25:23 -04:00
committed by GitHub
parent 88728ca1c1
commit 43856f261f
40 changed files with 1687 additions and 300 deletions
+3 -3
View File
@@ -108,7 +108,7 @@ import {RootstockSwapProvider} from './context-store/rootstockSwapContext';
import {SparkConnectionManager} from './context-store/sparkConnection';
import {GlobalNostrWalletConnectProvider} from './context-store/NWC';
import {GlobalServerTimeProvider} from './context-store/serverTime';
registerBackgroundNotificationTask();
const Stack = createNativeStackNavigator();
function App(): JSX.Element {
@@ -299,14 +299,14 @@ function ResetStack(): JSX.Element | null {
await runSecureStoreMigrationV2();
const [
initialURL,
registerBackground,
// registerBackground,
loginModeType,
pin,
mnemonic,
securitySettings,
] = await Promise.all([
getInitialURL(),
registerBackgroundNotificationTask(),
// registerBackgroundNotificationTask(),
retrieveData(LOGIN_SECURITY_MODE_TYPE_KEY),
retrieveData('pinHash'),
retrieveData('encryptedMnemonic'),
+1 -1
View File
@@ -90,7 +90,7 @@ android {
applicationId "com.blitzwallet"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34
versionCode 36
versionName "0.5.3"
ndk {
abiFilters 'arm64-v8a', 'x86_64', 'x86', 'armeabi-v7a'// Exclude riscv64
@@ -1,4 +1,5 @@
import {
ScrollView,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
@@ -17,6 +18,7 @@ export default function ErrorScreen(props) {
const navigationFunction = props.route.params?.navigationFunction;
const customNavigator = props.route.params?.customNavigator;
const height = props.route.params?.height;
const navigate = useNavigation();
const {theme, darkModeType} = useGlobalThemeContext();
@@ -32,31 +34,30 @@ export default function ErrorScreen(props) {
};
return (
<TouchableWithoutFeedback onPress={handleNaviagation}>
<View style={styles.globalContainer}>
<TouchableWithoutFeedback>
<View
style={[
styles.content,
{
backgroundColor: theme ? backgroundOffset : backgroundColor,
},
]}>
<ThemeText styles={styles.headerText} content={errorMessage} />
<View
style={{
...styles.border,
backgroundColor:
theme && darkModeType ? COLORS.darkModeText : COLORS.primary,
}}
/>
<TouchableOpacity onPress={handleNaviagation}>
<ThemeText styles={styles.cancelButton} content={'OK'} />
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
<View style={styles.globalContainer}>
<View
style={[
styles.content,
{
backgroundColor: theme ? backgroundOffset : backgroundColor,
maxHeight: height || 200,
},
]}>
<ScrollView>
<ThemeText styles={styles.headerText} content={errorMessage} />
</ScrollView>
<View
style={{
...styles.border,
backgroundColor:
theme && darkModeType ? COLORS.darkModeText : COLORS.primary,
}}
/>
<TouchableOpacity onPress={handleNaviagation}>
<ThemeText styles={styles.cancelButton} content={'OK'} />
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</View>
);
}
@@ -71,6 +72,7 @@ const styles = StyleSheet.create({
width: '95%',
maxWidth: 300,
borderRadius: 8,
maxHeight: 400,
},
headerText: {
width: '100%',
@@ -49,8 +49,8 @@ export function SendRecieveBTNs({
wantedContent: 'customInputText',
message: `Transfer funds ${
btnType === 'send'
? 'from nostr connect to main wallet'
: 'from main to nostr connect wallet'
? 'from Nostr Wallet Connect to main wallet'
: 'from main to Nostr Wallet Connects wallet'
}`,
type: btnType,
returnLocation: 'NWCWallet',
@@ -8,11 +8,13 @@ import {encodeLNURL} from '../../../../functions/lnurl/bench32Formmater';
import GetThemeColors from '../../../../hooks/themeColors';
import {useToast} from '../../../../../context-store/toastManager';
import {copyToClipboard} from '../../../../functions';
import {useGlobalThemeContext} from '../../../../../context-store/theme';
export default function ChooseLNURLCopyFormat(props) {
const {showToast} = useToast();
const {theme, darkModeType} = useGlobalThemeContext();
const {globalContactsInformation} = useGlobalContacts();
const {backgroundOffset} = GetThemeColors();
const {backgroundOffset, backgroundColor} = GetThemeColors();
const lightningAddress = `${globalContactsInformation.myProfile.uniqueName}@blitz-wallet.com`;
const lightningString = `${encodeLNURL(
@@ -25,7 +27,11 @@ export default function ChooseLNURLCopyFormat(props) {
onPress={() => {
copyToClipboard(lightningAddress, showToast);
}}
style={{...styles.copyRow, backgroundColor: backgroundOffset}}>
style={{
...styles.copyRow,
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
}}>
<ThemeImage
styles={styles.copyIcon}
lightModeIcon={ICONS.clipboardBlue}
@@ -45,7 +51,11 @@ export default function ChooseLNURLCopyFormat(props) {
onPress={() => {
copyToClipboard(lightningString, showToast);
}}
style={{...styles.copyRow, backgroundColor: backgroundOffset}}>
style={{
...styles.copyRow,
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
}}>
<ThemeImage
styles={styles.copyIcon}
lightModeIcon={ICONS.clipboardBlue}
@@ -0,0 +1,203 @@
import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native';
import {
CENTER,
CONTENT_KEYBOARD_OFFSET,
ICONS,
NOSTR_NAME_REGEX,
} from '../../../../../constants';
import {
CustomKeyboardAvoidingView,
GlobalThemeView,
ThemeText,
} from '../../../../../functions/CustomElements';
import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar';
import {INSET_WINDOW_WIDTH, SIZES} from '../../../../../constants/theme';
import {useGlobalContextProvider} from '../../../../../../context-store/context';
import {useState} from 'react';
import CustomSearchInput from '../../../../../functions/CustomElements/searchInput';
import CustomButton from '../../../../../functions/CustomElements/button';
import {useNavigation} from '@react-navigation/native';
import {addNip5toCollection, isValidNip5Name} from '../../../../../../db';
import {npubToHex} from '../../../../../functions/nostr';
import {copyToClipboard} from '../../../../../functions';
import {useToast} from '../../../../../../context-store/toastManager';
import ThemeImage from '../../../../../functions/CustomElements/themeImage';
export default function Nip5VerificationPage() {
const {showToast} = useToast();
const navigate = useNavigation();
const {masterInfoObject, toggleMasterInfoObject} = useGlobalContextProvider();
const [isLoading, setIsLoading] = useState('');
const {name, pubkey} = masterInfoObject?.nip5Settings;
console.log(masterInfoObject?.nip5Settings);
const [inputs, setInputs] = useState({
name: name || '',
pubkey: pubkey || '',
});
const handleInputText = (value, identifier) => {
setInputs(prev => ({
...prev,
[identifier]: value,
}));
};
const saveNip5Information = async () => {
try {
setIsLoading(true);
if (!inputs.name) throw new Error('Name cannot be empty.');
if (!inputs.pubkey) throw new Error('Public key cannot be empty.');
if (inputs.name === name && inputs.pubkey === pubkey) return;
if (inputs.name.length > 60)
throw new Error('Name must be less than 60 characters');
const parsedName = inputs.name.trim();
if (!NOSTR_NAME_REGEX.test(parsedName))
throw new Error('Name can only include letters or numbers.');
const formattedHexData = npubToHex(inputs.pubkey);
if (!formattedHexData?.didWork) {
navigate.navigate('ErrorScreen', {
errorMessage: formattedHexData.error,
});
}
const isNameFree = await isValidNip5Name(inputs.name);
if (!isNameFree) throw new Error('Name already taken');
toggleMasterInfoObject({
nip5Settings: {
name: parsedName,
pubkey: formattedHexData.data,
},
});
await addNip5toCollection(
{
name: parsedName,
nameLower: parsedName.toLowerCase(),
pubkey: formattedHexData.data,
didUpdate: true,
},
masterInfoObject.uuid,
);
navigate.navigate('ErrorScreen', {
errorMessage:
'NIP-05 added successfully! Please note that it may take up to 24 hours to appear, as the list is updated once per day.',
});
} catch (err) {
console.log('Error saving nip5 information', err);
navigate.navigate('ErrorScreen', {errorMessage: err.message});
} finally {
setIsLoading(false);
}
};
return (
<GlobalThemeView useStandardWidth={true}>
<CustomKeyboardAvoidingView useTouchableWithoutFeedback={true}>
<View style={StyleSheet.absoluteFill}>
<CustomSettingsTopBar
shouldDismissKeyboard={true}
label={'Nip5 Verification'}
/>
<ScrollView showsVerticalScrollIndicator={false}>
<ThemeText
styles={styles.explainerText}
content={
'Nip5 turns your long Npub into a small email-like address, similar to a lightning address.'
}
/>
<View style={styles.inputRow}>
<ThemeText styles={styles.inputDescriptor} content={'Username'} />
<CustomSearchInput
inputText={inputs.name}
setInputText={e => handleInputText(e, 'name')}
placeholderText="Satoshi..."
/>
<ThemeText
styles={styles.textCoount}
content={`${inputs.name.length}/60`}
/>
</View>
<View style={styles.inputRow}>
<ThemeText
styles={styles.inputDescriptor}
content={'Public key'}
/>
<CustomSearchInput
inputText={inputs.pubkey}
setInputText={e => handleInputText(e, 'pubkey')}
placeholderText="Npub..."
/>
</View>
<TouchableOpacity
style={styles.nip5AddressContainer}
onPress={() => {
if (!inputs.name.length) return;
copyToClipboard(`${inputs.name}@blitz-wallet.com`, showToast);
}}>
<ThemeImage
styles={{width: 25, height: 25}}
lightModeIcon={ICONS.clipboardBlue}
darkModeIcon={ICONS.clipboardBlue}
lightsOutIcon={ICONS.clipboardLight}
/>
<ThemeText
CustomNumberOfLines={1}
styles={styles.nip5AddressText}
content={`${
inputs.name.length ? inputs.name : '...'
}@blitz-wallet.com`}
/>
</TouchableOpacity>
</ScrollView>
<CustomButton
useLoading={isLoading}
actionFunction={saveNip5Information}
buttonStyles={{...CENTER, marginBottom: CONTENT_KEYBOARD_OFFSET}}
textContent={inputs.name || inputs.pubkey ? 'Update' : 'Save'}
/>
</View>
</CustomKeyboardAvoidingView>
</GlobalThemeView>
);
}
const styles = StyleSheet.create({
contentcontainer: {},
explainerText: {
width: INSET_WINDOW_WIDTH,
...CENTER,
textAlign: 'center',
marginTop: 20,
marginBottom: 50,
},
inputRow: {
width: '90%',
...CENTER,
marginVertical: 10,
},
inputDescriptor: {
marginBottom: 10,
},
textCoount: {
width: '100%',
textAlign: 'right',
marginTop: 5,
},
nip5AddressContainer: {
width: '90%',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
marginTop: 20,
...CENTER,
marginBottom: 10,
},
nip5AddressText: {
fontSize: SIZES.large,
textAlign: 'center',
flexShrink: 1,
},
});
@@ -0,0 +1,108 @@
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import {ThemeText} from '../../../../functions/CustomElements';
import {COLORS, INSET_WINDOW_WIDTH, SIZES} from '../../../../constants/theme';
import {CENTER, ICONS} from '../../../../constants';
import ThemeImage from '../../../../functions/CustomElements/themeImage';
import GetThemeColors from '../../../../hooks/themeColors';
import {useGlobalThemeContext} from '../../../../../context-store/theme';
import {useNavigation} from '@react-navigation/native';
export default function NostrHome() {
const navitate = useNavigation();
const {darkModeType, theme} = useGlobalThemeContext();
const {backgroundOffset, backgroundColor} = GetThemeColors();
return (
<View style={styles.container}>
<View
style={{
...styles.itemRow,
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
}}>
<View style={styles.itemTextContainer}>
<ThemeText styles={styles.itemHeader} content={'Nip5 Verification'} />
<ThemeText
styles={styles.itemDescription}
content={'Quickly verify your Nostr identity using Blitz.'}
/>
</View>
<TouchableOpacity
onPress={() => navitate.navigate('Nip5VerificationPage')}
style={{
...styles.clickContainer,
backgroundColor: theme ? backgroundColor : COLORS.primary,
}}>
<ThemeImage
styles={{transform: [{rotate: '180deg'}]}}
lightModeIcon={ICONS.leftCheveronLight}
darkModeIcon={ICONS.leftCheveronLight}
lightsOutIcon={ICONS.leftCheveronLight}
/>
</TouchableOpacity>
</View>
<View
style={{
...styles.itemRow,
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
}}>
<View style={styles.itemTextContainer}>
<ThemeText
styles={styles.itemHeader}
content={'Nostr Wallet Connect'}
/>
<ThemeText
styles={styles.itemDescription}
content={'Connect your Blitz Wallet to apps using Nostr.'}
/>
</View>
<TouchableOpacity
onPress={() => {
navitate.navigate('NosterWalletConnect');
}}
style={{
...styles.clickContainer,
backgroundColor: theme ? backgroundColor : COLORS.primary,
}}>
<ThemeImage
styles={{transform: [{rotate: '180deg'}]}}
lightModeIcon={ICONS.leftCheveronLight}
darkModeIcon={ICONS.leftCheveronLight}
lightsOutIcon={ICONS.leftCheveronLight}
/>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
width: INSET_WINDOW_WIDTH,
...CENTER,
},
itemRow: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
marginVertical: 10,
padding: 10,
borderRadius: 8,
justifyContent: 'space-between',
},
itemTextContainer: {
flexShrink: 1,
marginRight: 15,
},
itemHeader: {
marginBottom: 10,
includeFontPadding: false,
},
itemDescription: {fontSize: SIZES.small, includeFontPadding: false},
clickContainer: {
borderRadius: 8,
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
});
@@ -152,13 +152,13 @@ export default function NotificationPreferances() {
}
containerStyles={styles.toggleContainers}
/>
{/* <SettingsItemWithSlider
settingsTitle={`Nostr Connect`}
<SettingsItemWithSlider
settingsTitle={`Nostr Wallet Connect`}
showDescription={false}
handleSubmit={() => toggleNotificationPreferance('NWC')}
toggleSwitchStateValue={notificationData.enabledServices.NWC}
containerStyles={styles.toggleContainers}
/> */}
/>
<SettingsItemWithSlider
settingsTitle={`Point-of-sale`}
showDescription={false}
@@ -16,6 +16,7 @@ import {usePushNotification} from '../../../../../context-store/notificationMana
import NostrWalletConnectNoNotifications from './nwc/noNotifications';
import {
CustomKeyboardAvoidingView,
GlobalThemeView,
ThemeText,
} from '../../../../functions/CustomElements';
import CustomSearchInput from '../../../../functions/CustomElements/searchInput';
@@ -27,6 +28,7 @@ import Icon from '../../../../functions/CustomElements/Icon';
import {retrieveData} from '../../../../functions';
import NWCWalletSetup from './nwc/showSeedPage';
import HasNoNostrAccounts from './nwc/hasNoAccounts';
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
export default function NosterWalletConnect() {
const navigate = useNavigation();
@@ -65,15 +67,27 @@ export default function NosterWalletConnect() {
};
if (!hasSeenMnemoinc) {
return <NWCWalletSetup />;
return (
<CustomPageWrapper>
<NWCWalletSetup setHasSeenMnemoinc={setHasSeenMnemoinc} />
</CustomPageWrapper>
);
}
if (!hasEnabledPushNotifications) {
return <NostrWalletConnectNoNotifications />;
return (
<CustomPageWrapper>
<NostrWalletConnectNoNotifications />
</CustomPageWrapper>
);
}
if (!didViewWarningMessage) {
return <HasNoNostrAccounts />;
return (
<CustomPageWrapper>
<HasNoNostrAccounts />
</CustomPageWrapper>
);
}
const nwcElements = savedNWCAccounts?.accounts
@@ -150,52 +164,63 @@ export default function NosterWalletConnect() {
: [];
return (
<CustomKeyboardAvoidingView
useTouchableWithoutFeedback={true}
globalThemeViewStyles={{
paddingTop: 10,
width: INSET_WINDOW_WIDTH,
...CENTER,
}}>
<CustomSearchInput
inputText={accountName}
setInputText={setAccountName}
placeholderText={'Search for NWC account'}
/>
<ScrollView contentContainerStyle={{paddingBottom: 20}}>
{nwcElements.length > 0 ? (
nwcElements
) : (
<ThemeText
styles={{textAlign: 'center', marginTop: 20}}
content={'You have no Nostr Connect accounts.'}
/>
)}
</ScrollView>
<View
style={{
width: '100%',
columnGap: 10,
rowGap: 10,
flexWrap: 'wrap',
flexDirection: 'row',
<CustomPageWrapper>
<CustomKeyboardAvoidingView
useTouchableWithoutFeedback={true}
globalThemeViewStyles={{
paddingTop: 10,
width: INSET_WINDOW_WIDTH,
...CENTER,
}}>
<CustomButton
actionFunction={() => {
navigate.navigate('CreateNostrConnectAccount');
}}
buttonStyles={{flexGrow: 1, maxWidth: '48%'}}
textContent={'Add Account'}
<CustomSearchInput
inputText={accountName}
setInputText={setAccountName}
placeholderText={'Search for NWC account'}
/>
<CustomButton
actionFunction={() => {
navigate.navigate('NWCWallet');
}}
buttonStyles={{flexGrow: 1, maxWidth: '48%'}}
textContent={'View Wallet'}
/>
</View>
</CustomKeyboardAvoidingView>
<ScrollView contentContainerStyle={{paddingBottom: 20}}>
{nwcElements.length > 0 ? (
nwcElements
) : (
<ThemeText
styles={{textAlign: 'center', marginTop: 20}}
content={'You have no Nostr Connect accounts.'}
/>
)}
</ScrollView>
<View
style={{
width: '100%',
columnGap: 10,
rowGap: 10,
flexWrap: 'wrap',
flexDirection: 'row',
}}>
<CustomButton
actionFunction={() => {
navigate.navigate('CreateNostrConnectAccount');
}}
buttonStyles={{flexGrow: 1, maxWidth: '48%'}}
textContent={'Add Account'}
/>
<CustomButton
actionFunction={() => {
navigate.navigate('NWCWallet');
}}
buttonStyles={{flexGrow: 1, maxWidth: '48%'}}
textContent={'View Wallet'}
/>
</View>
</CustomKeyboardAvoidingView>
</CustomPageWrapper>
);
}
function CustomPageWrapper({children}) {
return (
<GlobalThemeView useStandardWidth={true}>
<CustomSettingsTopBar label={'NWC'} />
{children}
</GlobalThemeView>
);
}
@@ -25,14 +25,15 @@ import {
getNWCSparkIdentityPubKey,
getNWCSparkTransactions,
initializeNWCWallet,
sendNWCSparkPayment,
sendNWCSparkLightningPayment,
} from '../../../../../functions/nwc/wallet';
import {useKeysContext} from '../../../../../../context-store/keys';
import {useNostrWalletConnect} from '../../../../../../context-store/NWC';
import {useSparkWallet} from '../../../../../../context-store/sparkContext';
import {sparkPaymenWrapper} from '../../../../../functions/spark/payments';
import FullLoadingScreen from '../../../../../functions/CustomElements/loadingScreen';
import {useGlobalInsets} from '../../../../../../context-store/insetsProvider';
import {useGlobalContacts} from '../../../../../../context-store/globalContacts';
import {getBolt11InvoiceForContact} from '../../../../../functions/contacts';
export default function NWCWallet(props) {
const {bottomPadding} = useGlobalInsets();
@@ -40,6 +41,7 @@ export default function NWCWallet(props) {
const {nwcWalletInfo, setNWCWalletInfo} = useNostrWalletConnect();
const {theme, darkModeType, toggleTheme} = useGlobalThemeContext();
const {masterInfoObject} = useGlobalContextProvider();
const {globalContactsInformation} = useGlobalContacts();
const {isConnectedToTheInternet} = useAppStatus();
const navigate = useNavigation();
const currentTime = useUpdateHomepageTransactions();
@@ -115,13 +117,33 @@ export default function NWCWallet(props) {
if (!sendingAmount || !sendingType) return;
try {
setIsTranfering(true);
let address = '';
if (sendingType === 'send') {
const invoiceResponse = await getBolt11InvoiceForContact(
globalContactsInformation.myProfile.uniqueName,
sendingAmount,
'Transfer from nostr connect',
false,
);
console.log(invoiceResponse);
if (!invoiceResponse) {
navigate.navigate('ErrorScreen', {
errorMessage:
'Unable to generate the receiving invoice. Please try again.',
});
return;
}
address = invoiceResponse;
} else {
address = nwcWalletInfo.sparkAddress;
}
const fee = await sparkPaymenWrapper({
getFee: true,
address:
sendingType === 'send'
? nwcWalletInfo.sparkAddress
: sparkInformation.sparkAddress,
paymentType: 'spark',
address,
paymentType: sendingType === 'send' ? 'lightning' : 'spark',
amountSats: sendingAmount,
masterInfoObject,
});
@@ -146,13 +168,13 @@ export default function NWCWallet(props) {
let response;
if (sendingType === 'send') {
response = await sendNWCSparkPayment(
sendingAmount,
sparkInformation.sparkAddress,
);
response = await sendNWCSparkLightningPayment({
invoice: address,
maxFeeSats: fee.fee,
});
} else {
response = await sparkPaymenWrapper({
address: nwcWalletInfo.sparkAddress,
address,
paymentType: 'spark',
amountSats: sendingAmount,
masterInfoObject,
@@ -181,7 +203,7 @@ export default function NWCWallet(props) {
return (
<GlobalThemeView styles={style.globalContainer}>
<CustomSettingsTopBar
containerStyles={{width: INSET_WINDOW_WIDTH, ...CENTER}}
containerStyles={{width: '95%', ...CENTER}}
showLeftImage={true}
leftImageBlue={ICONS.keyIcon}
LeftImageDarkMode={ICONS.keyIconWhite}
@@ -5,7 +5,7 @@ import {
} from '../../../../../functions/CustomElements';
import CustomSearchInput from '../../../../../functions/CustomElements/searchInput';
import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar';
import {useState} from 'react';
import {useEffect, useState} from 'react';
import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native';
import {COLORS, INSET_WINDOW_WIDTH} from '../../../../../constants/theme';
import {CENTER} from '../../../../../constants';
@@ -22,9 +22,10 @@ import {createAccountMnemonic} from '../../../../../functions';
import * as nostr from 'nostr-tools';
import crypto from 'react-native-quick-crypto';
import sha256Hash from '../../../../../functions/hash';
import {nwc} from '@getalby/sdk';
import {getSupportedMethods} from '../../../../../functions/nwc';
import {privateKeyFromSeedWords} from '../../../../../functions/nostrCompatability';
import {useGlobalInsets} from '../../../../../../context-store/insetsProvider';
import {publishToSingleRelay} from '../../../../../functions/nwc/publishResponse';
const BUDGET_RENEWAL_OPTIONS = [
{label: 'Daily', value: 'Daily'},
@@ -37,17 +38,23 @@ const BUDGET_AMOUNT_OPTIONS = [50_000, 100_000, 'Unlimited', 'Custom...'];
export default function CreateNostrConnectAccount(props) {
const navigate = useNavigation();
const {masterInfoObject, toggleNWCInformation} = useGlobalContextProvider();
const isEditing = props.route?.params?.accountID;
const savedData = props.route?.params?.data;
const passedParams = props.route?.params;
const isEditing = passedParams?.accountID;
const savedData = passedParams?.data;
const {fiatStats} = useNodeContext();
const [accountName, setAccountName] = useState(
isEditing ? savedData.accountName : '',
);
const [outerScrollEnabled, setOuterScrollEnabled] = useState(true);
const [accountPermissions, setAccountPermissions] = useState({
receivePayments: isEditing ? savedData.permissions.receivePayments : false,
sendPayments: isEditing ? savedData.permissions.sendPayments : false,
getBalance: isEditing ? savedData.permissions.getBalance : false,
transactionHistory: isEditing
? savedData.permissions.transactionHistory
: false,
lookupInvoice: isEditing ? savedData.permissions.lookupInvoice : false,
});
const [budgetRenewalSettings, setBudgetRenewalSettings] = useState({
option: isEditing ? savedData.budgetRenewalSettings.option : null,
@@ -56,9 +63,10 @@ export default function CreateNostrConnectAccount(props) {
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const [isCreatingAccount, setIsCreatingAccount] = useState(false);
const {textColor, backgroundOffset} = GetThemeColors();
const {bottomPadding} = useGlobalInsets();
const {theme, darkModeType} = useGlobalThemeContext();
console.log(accountPermissions, 'account permission');
const handleDropdownScrollStart = () => {
setOuterScrollEnabled(false);
};
@@ -76,6 +84,10 @@ export default function CreateNostrConnectAccount(props) {
savedData.permissions.receivePayments ===
accountPermissions.receivePayments &&
savedData.permissions.sendPayments === accountPermissions.sendPayments &&
savedData.permissions.lookupInvoice ===
accountPermissions.lookupInvoice &&
savedData.permissions.transactionHistory ===
accountPermissions.transactionHistory &&
savedData.permissions.getBalance === accountPermissions.getBalance &&
savedData.budgetRenewalSettings.option === budgetRenewalSettings.option &&
savedData.budgetRenewalSettings.amount === budgetRenewalSettings.amount
@@ -92,7 +104,9 @@ export default function CreateNostrConnectAccount(props) {
if (
!accountPermissions.receivePayments &&
!accountPermissions.sendPayments &&
!accountPermissions.getBalance
!accountPermissions.getBalance &&
!accountPermissions.transactionHistory &&
!accountPermissions.lookupInvoice
) {
navigate.navigate('ErrorScreen', {
errorMessage: 'Please enable at least one permission.',
@@ -129,20 +143,20 @@ export default function CreateNostrConnectAccount(props) {
secret = savedData.secret;
}
console.log('Generated mnemonic:', mnemonic, privateKey, publicKey);
const infoEvent = {
kind: 13194,
created_at: Math.floor(Date.now() / 1000),
content: getSupportedMethods(accountPermissions).join(' '),
tags: [],
};
console.log(accountName, accountPermissions, budgetRenewalSettings);
const walletService = new nwc.NWCWalletService({
relayUrl: 'wss://relay.damus.io',
});
await walletService.publishWalletServiceInfoEvent(
secret,
getSupportedMethods(accountPermissions),
[],
const signedEvent = nostr.finalizeEvent(
infoEvent,
Buffer.from(privateKey, 'hex'),
);
await publishToSingleRelay([signedEvent], 'wss://relay.damus.io');
toggleNWCInformation({
accounts: {
...(masterInfoObject?.NWC?.accounts || {}),
@@ -165,6 +179,18 @@ export default function CreateNostrConnectAccount(props) {
setIsCreatingAccount(false);
}
};
console.log(budgetRenewalSettings);
useEffect(() => {
if (props?.route?.params?.amount) {
setBudgetRenewalSettings(prev => ({
...prev,
amount: props?.route?.params?.amount,
}));
}
}, [props?.route?.params?.amount]);
console.log(props?.route?.params, 'test');
const budgetElements = BUDGET_AMOUNT_OPTIONS.map(option => {
return (
@@ -174,19 +200,22 @@ export default function CreateNostrConnectAccount(props) {
navigate.navigate('CustomHalfModal', {
wantedContent: 'customInputText',
// sliderHight: 0.5,
returnLocation: 'CreateNostrConnectAccount',
passedParams,
});
return;
}
if (props?.route?.params?.amount) {
navigate.setParams({amount: ''});
}
navigate.setParams({
amount: '',
});
setBudgetRenewalSettings(prev => ({
...prev,
amount: prev.amount === option ? '' : option,
}));
}}
style={{
maxWidth: '48%',
// maxWidth: '48%',
minWidth: '48%',
flexGrow: 1,
borderWidth: 1,
borderColor:
@@ -205,6 +234,7 @@ export default function CreateNostrConnectAccount(props) {
key={option.toString()}>
{typeof option === 'number' ? (
<ThemeText
styles={{includeFontPadding: false}}
content={displayCorrectDenomination({
amount: option,
masterInfoObject,
@@ -213,6 +243,7 @@ export default function CreateNostrConnectAccount(props) {
/>
) : (
<ThemeText
styles={{includeFontPadding: false}}
content={
option === 'Custom...' && props?.route?.params?.amount
? displayCorrectDenomination({
@@ -248,6 +279,7 @@ export default function CreateNostrConnectAccount(props) {
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingTop: 10,
paddingBottom: 20,
width: INSET_WINDOW_WIDTH,
...CENTER,
}}>
@@ -274,6 +306,7 @@ export default function CreateNostrConnectAccount(props) {
}
toggleSwitchStateValue={accountPermissions.receivePayments}
containerStyles={styles.toggleContainers}
switchPageName="nwcAccount"
/>
<SettingsItemWithSlider
settingsTitle={`Send payments`}
@@ -286,6 +319,7 @@ export default function CreateNostrConnectAccount(props) {
}
toggleSwitchStateValue={accountPermissions.sendPayments}
containerStyles={styles.toggleContainers}
switchPageName="nwcAccount"
/>
<SettingsItemWithSlider
settingsTitle={`Get balance`}
@@ -297,7 +331,34 @@ export default function CreateNostrConnectAccount(props) {
}))
}
toggleSwitchStateValue={accountPermissions.getBalance}
containerStyles={{marginTop: 0}}
switchPageName="nwcAccount"
/>
<SettingsItemWithSlider
settingsTitle={`Transactions`}
showDescription={false}
handleSubmit={() =>
setAccountPermissions(prev => ({
...prev,
transactionHistory: !prev.transactionHistory,
}))
}
toggleSwitchStateValue={accountPermissions.transactionHistory}
containerStyles={{marginTop: 0}}
switchPageName="nwcAccount"
/>
<SettingsItemWithSlider
settingsTitle={`Lookup Invoice`}
showDescription={false}
handleSubmit={() =>
setAccountPermissions(prev => ({
...prev,
lookupInvoice: !prev.lookupInvoice,
}))
}
toggleSwitchStateValue={accountPermissions.lookupInvoice}
containerStyles={{marginTop: 0, marginBottom: 0}}
switchPageName="nwcAccount"
/>
<ThemeText
styles={{marginTop: 30, marginBottom: 10}}
@@ -331,7 +392,9 @@ export default function CreateNostrConnectAccount(props) {
{!isKeyboardActive && (
<CustomButton
actionFunction={handleAccountCreation}
buttonStyles={{...CENTER}}
buttonStyles={{
...CENTER,
}}
textContent={'Save'}
/>
)}
@@ -12,7 +12,7 @@ export default function HasNoNostrAccounts() {
<ThemeText
styles={{textAlign: 'center', marginBottom: 50}}
content={
'To keep your main wallet safe, Nostr Connect makes a separate wallet using a key from your original wallets seed, based on the 2nd index.\n\nTTo send money from your Nostr Connect wallet, first add funds to it by receiving money or transferring some from your main wallet.'
'To send money from your Nostr Connect wallet, first add funds to it by receiving money or transferring some from your main wallet.'
}
/>
<CustomButton
@@ -11,7 +11,7 @@ export default function NostrWalletConnectNoNotifications() {
<ThemeText
styles={styles.textStyles}
content={
'In order to use Nostr Connect you need to have push notification for Nostr Connect enabled.\n\nPlease enable push notifications in the settings and try again.'
'In order to use Nostr Wallet Connect you need to have push notification for Nostr Wallet Connect enabled.\n\nPlease enable push notifications in the settings and try again.'
}
/>
<CustomButton
@@ -139,7 +139,7 @@ export default function NWCWalletSetup(props) {
}}
textStyles={{color: COLORS.darkModeText}}
textContent={'Continue'}
actionFunction={navigate.goBack}
actionFunction={() => props.setHasSeenMnemoinc(true)}
/>
)}
</View>
@@ -156,7 +156,7 @@ export default function NWCWalletSetup(props) {
<View style={styles.confirmPopupInnerContainer}>
<ThemeText
styles={styles.confirmPopupTitle}
content={`To keep your wallet safe, Nostr Connect creates a separate seed phrase from your main wallet's seed.\n\nThis wallet uses the second derivation path and can always be recovered using your main wallet's seed.\n\nIf you're not tech-savvy and unsure about recovering the wallet address, please write down this seed phrase.`}
content={`To keep your wallet safe, Nostr Wallet Connect creates a separate seed phrase from your main wallet's seed.\n\nThis wallet uses the second derivation path and can always be recovered using your main wallet's seed.\n\nIf you're not tech-savvy and unsure about recovering the wallet address, please write down this seed phrase.`}
/>
<View style={styles.confirmationContainer}>
<CustomButton
+3
View File
@@ -34,6 +34,8 @@ const IS_SPARK_REQUEST_ID =
/^SparkLightning(?:Receive|Send)Request:[0-9a-fA-F\-]+$/;
const IS_BITCOIN_REQUEST_ID = /^SparkCoopExitRequest:[0-9a-fA-F\-]+$/;
const NOSTR_NAME_REGEX = /^[a-zA-Z0-9]+$/;
const IS_LETTER_REGEX = /^[A-Za-z]$/;
const BITCOIN_SATS_ICON = '\u20BF';
const HIDDEN_BALANCE_TEXT = `* * * * *`;
@@ -135,4 +137,5 @@ export {
NWC_LOACAL_STORE_KEY,
SPARK_CACHED_BALANCE_KEY,
IS_SPARK_ID,
NOSTR_NAME_REGEX,
};
@@ -49,10 +49,18 @@ export default function CustomInputHalfModal(props) {
if (!amountValue) {
return;
}
navigate.popTo(props.returnLocation, {
amount: localSatAmount,
type: props.type,
});
if (props?.passedParams) {
navigate.popTo(props.returnLocation, {
...props?.passedParams,
amount: localSatAmount,
type: props.type,
});
} else {
navigate.popTo(props.returnLocation, {
amount: localSatAmount,
type: props.type,
});
}
};
return (
+44 -12
View File
@@ -1,12 +1,12 @@
import React, {useState, useRef} from 'react';
import {
View,
Text,
TouchableOpacity,
ScrollView,
StyleSheet,
Dimensions,
Modal,
Platform,
} from 'react-native';
import ThemeImage from './themeImage';
import {COLORS, ICONS} from '../../constants';
@@ -26,6 +26,7 @@ const DropdownMenu = ({
const dropdownRef = useRef(null);
const {theme, darkModeType} = useGlobalThemeContext();
const {backgroundOffset, backgroundColor} = GetThemeColors();
const [dropdownHeight, setDropdownHeight] = useState(0);
const handleSelect = item => {
onSelect(item);
@@ -36,12 +37,37 @@ const DropdownMenu = ({
setButtonLayout(event.nativeEvent.layout);
};
const measureButtonPosition = () => {
return new Promise(resolve => {
if (dropdownRef.current) {
dropdownRef.current.measure((x, y, width, height, pageX, pageY) => {
const layout = {
x: pageX,
y: pageY,
width,
height,
};
setButtonLayout(layout);
resolve(layout);
});
} else {
resolve(buttonLayout);
}
});
};
const handleDropdownToggle = async () => {
if (!isOpen) {
// Recalculate position before opening
await measureButtonPosition();
}
setIsOpen(!isOpen);
};
// Calculate if dropdown should open upwards based on screen position
const screenHeight = Dimensions.get('window').height;
const dropdownHeight = 200; // Max height of dropdown menu
const isTooLow =
buttonLayout &&
buttonLayout.y + buttonLayout.height + dropdownHeight > screenHeight;
buttonLayout.y + buttonLayout.height + dropdownHeight + 50 > screenHeight;
return (
<View style={styles.container} ref={dropdownRef} onLayout={handleLayout}>
@@ -58,11 +84,11 @@ const DropdownMenu = ({
...styles.dropdownButton,
backgroundColor: theme ? backgroundOffset : COLORS.darkModeText,
}}
onPress={() => setIsOpen(!isOpen)}>
onPress={() => handleDropdownToggle()}>
<ThemeText
styles={{
includeFontPadding: false,
flexGrow: 1,
flexShrink: 1,
}}
CustomNumberOfLines={1}
content={selectedValue ? selectedValue : placeholder}
@@ -123,20 +149,24 @@ const DropdownMenu = ({
style={[
styles.dropdownMenu,
buttonLayout && {
top: isTooLow ? buttonLayout.y - 105 : buttonLayout.y + 145,
top: isTooLow
? buttonLayout.y - dropdownHeight - 5
: buttonLayout.y + (Platform.OS === 'ios' ? 50 : 25),
left: '7.3%',
width: selectorLayout?.width,
},
{backgroundColor: theme ? backgroundOffset : COLORS.darkModeText},
]}>
<ScrollView>
{options.map(item => (
<ScrollView
onLayout={e => {
setDropdownHeight(e.nativeEvent.layout.height);
}}>
{options.map((item, index) => (
<TouchableOpacity
key={item.value.toString()}
style={{
...styles.dropdownItem,
backgroundColor: theme
? backgroundOffset
: COLORS.darkModeText,
borderBottomWidth: index !== options.length - 1 ? 1 : 0,
borderBottomColor: backgroundColor,
}}
onPress={() => handleSelect(item)}>
@@ -181,7 +211,9 @@ const styles = StyleSheet.create({
overflow: 'hidden',
},
dropdownItem: {
padding: 12,
height: 45,
justifyContent: 'center',
paddingHorizontal: 10,
borderBottomWidth: 1,
},
});
@@ -240,6 +240,7 @@ export default function CustomHalfModal(props) {
message={props?.route?.params?.message}
type={props?.route?.params?.type}
returnLocation={props?.route?.params?.returnLocation}
passedParams={props?.route?.params?.passedParams}
/>
);
case 'chooseLNURLCopyFormat':
@@ -48,48 +48,46 @@ export default function SettingsItemWithSlider({
]}>
<ThemeText
CustomNumberOfLines={1}
styles={{
...styles.settingsTitle,
flex: showInformationPopup ? 0 : 1,
marginRight: showInformationPopup ? 5 : 0,
}}
styles={styles.settingsTitle}
content={settingsTitle}
/>
{showLoadingIcon && (
<FullLoadingScreen
containerStyles={{
...styles.loadingContainer,
marginLeft: showInformationPopup ? 5 : 10,
marginRight: showInformationPopup ? 5 : 'auto',
}}
size="small"
showText={false}
loadingColor={theme ? textColor : COLORS.primary}
/>
)}
{showInformationPopup && (
<TouchableOpacity
onPress={() => {
navigate.navigate('InformationPopup', {
textContent: informationPopupText,
buttonText: informationPopupBTNText,
});
}}
style={styles.imageContainer}>
<ThemeImage
styles={styles.themeImage}
lightModeIcon={ICONS.aboutIcon}
darkModeIcon={ICONS.aboutIcon}
lightsOutIcon={ICONS.aboutIconWhite}
<View style={styles.rightItemContainer}>
{showLoadingIcon && (
<FullLoadingScreen
containerStyles={{
...styles.loadingContainer,
marginLeft: showInformationPopup ? 5 : 10,
marginRight: showInformationPopup ? 5 : 'auto',
}}
size="small"
showText={false}
loadingColor={theme ? textColor : COLORS.primary}
/>
</TouchableOpacity>
)}
)}
{showInformationPopup && (
<TouchableOpacity
onPress={() => {
navigate.navigate('InformationPopup', {
textContent: informationPopupText,
buttonText: informationPopupBTNText,
});
}}
style={styles.imageContainer}>
<ThemeImage
styles={styles.themeImage}
lightModeIcon={ICONS.aboutIcon}
darkModeIcon={ICONS.aboutIcon}
lightsOutIcon={ICONS.aboutIconWhite}
/>
</TouchableOpacity>
)}
<CustomToggleSwitch
toggleSwitchFunction={handleSubmit}
page={switchPageName}
stateValue={toggleSwitchStateValue}
/>
<CustomToggleSwitch
toggleSwitchFunction={handleSubmit}
page={switchPageName}
stateValue={toggleSwitchStateValue}
/>
</View>
</View>
{showDescription && (
<View style={styles.textContainer}>
@@ -123,9 +121,16 @@ const styles = StyleSheet.create({
marginLeft: 20,
},
settingsTitle: {
flex: 1,
flexShrink: 1,
includeFontPadding: false,
},
rightItemContainer: {
alignItems: 'center',
flexDirection: 'row',
flexGrow: 1,
marginLeft: 5,
justifyContent: 'flex-end',
},
themeText: {
includeFontPadding: false,
},
@@ -138,7 +143,6 @@ const styles = StyleSheet.create({
},
loadingContainer: {
alignItems: 'left',
flex: 0,
},
});
+2 -1
View File
@@ -91,7 +91,8 @@ const CustomToggleSwitch = ({
page === 'hideUnknownContacts' ||
page === 'useTrampoline' ||
page === 'LoginSecurityMode' ||
page === 'fastPay'
page === 'fastPay' ||
page === 'nwcAccount'
? backgroundColor
: backgroundOffset,
darkModeType && theme ? COLORS.darkModeText : COLORS.primary,
+3 -2
View File
@@ -13,6 +13,7 @@ export async function getBolt11InvoiceForContact(
contactUniqueName,
sendingValue,
description,
useBlitzContact = true,
) {
try {
let runCount = 0;
@@ -23,12 +24,12 @@ export async function getBolt11InvoiceForContact(
try {
const url = `https://blitz-wallet.com/.well-known/lnurlp/${contactUniqueName}?amount=${
sendingValue * 1000
}&isBlitzContact=true${
}&isBlitzContact=${useBlitzContact ? true : false}${
!!description
? `&comment=${encodeURIComponent(description || '')}`
: ''
}`;
console.log(url);
const response = await fetch(url);
const data = await response.json();
if (data.status !== 'OK') throw new Error('Not able to get invoice');
+5
View File
@@ -176,6 +176,10 @@ export default async function initializeUserSettingsFromHistory({
lastUpdated: new Date().getTime(),
addresses: [],
};
const nip5Settings = blitzStoredData.nip5Settings || {
name: '',
pubkey: '',
};
// let lnurlPubKey = blitzStoredData.lnurlPubKey;
@@ -321,6 +325,7 @@ export default async function initializeUserSettingsFromHistory({
// store in contacts context
tempObject['contacts'] = contacts;
tempObject['NWC'] = savedNWCData;
tempObject['nip5Settings'] = nip5Settings;
// Store in ecash context
// tempObject['eCashInformation'] = eCashInformation;
+44
View File
@@ -0,0 +1,44 @@
import {nip19} from 'nostr-tools';
export function isValidNpub(npub) {
try {
const decoded = nip19.decode(npub);
return decoded.type === 'npub';
} catch (error) {
console.log('error validating npub', error);
return false;
}
}
export function npubToHex(pubkey) {
try {
if (!pubkey || typeof pubkey !== 'string') {
throw new Error('Invalid pubkey: must be a non-empty string');
}
const cleanPubkey = pubkey.trim();
if (cleanPubkey.startsWith('npub1')) {
try {
const decoded = nip19.decode(cleanPubkey);
if (decoded.type === 'npub' && typeof decoded.data === 'string') {
return {didWork: true, data: decoded.data};
}
throw new Error('Invalid npub format');
} catch (error) {
throw new Error(`Failed to decode npub: ${error.message}`);
}
}
const hexRegex = /^[0-9a-fA-F]{64}$/;
if (hexRegex.test(cleanPubkey)) {
return {didWork: true, data: cleanPubkey.toLowerCase()};
}
throw new Error(
'Invalid pubkey format: must be either 64-character hex string or npub',
);
} catch (err) {
return {didWork: false, error: err.message};
}
}
+35
View File
@@ -0,0 +1,35 @@
import * as Notifications from 'expo-notifications';
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
/**
* Pushes an instant local notification
* @param {string} message - The notification message to display
* @param {string} title - Optional title for the notification (defaults to "Notification")
* @returns {Promise<string>} - Returns the notification identifier
*/
export const pushInstantNotification = async (message, title = '') => {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: title,
body: message,
sound: 'default',
},
trigger: null, // null trigger means immediate notification
});
console.log('Notification sent with ID:', notificationId);
return notificationId;
} catch (error) {
console.error('Error sending notification:', error);
throw error;
}
};
+247 -2
View File
@@ -11,7 +11,9 @@ import {
} from '../messaging/encodingAndDecodingMessages';
import {publishToSingleRelay} from './publishResponse';
import {
getNWCLightningReceiveRequest,
getNWCSparkBalance,
getNWCSparkTransactions,
initializeNWCWallet,
NWCSparkLightningPaymentStatus,
receiveNWCSparkLightningPayment,
@@ -19,6 +21,9 @@ import {
} from './wallet';
import sha256Hash from '../hash';
import bolt11 from 'bolt11';
import {getSparkPaymentStatus, sparkPaymentType} from '../spark';
import {pushInstantNotification} from '../notifications';
import NWCInvoiceManager from './cachedNWCTxs';
// const handledEventIds = new Set();
let nwcAccounts, fullStorageObject;
@@ -111,6 +116,102 @@ const handleGetInfo = selectedNWCAccount => ({
},
});
const handleGetTransactions = async requestParams => {
const connectResponse = await ensureWalletConnection();
if (!connectResponse.isConnected) {
return createErrorResponse(
'list_transactions',
ERROR_CODES.INTERNAL,
'Unable to connect to wallet',
);
}
const {from, until, limit = 20, offset = 0, type} = requestParams;
let allTransactions = [];
let currentOffset = 0;
const chunkSize = 100;
let hasMore = true;
while (hasMore) {
const chunk = await getNWCSparkTransactions(chunkSize, currentOffset);
if (!chunk || chunk.transfers.length === 0) {
hasMore = false;
break;
}
allTransactions = allTransactions.concat(chunk.transfers);
currentOffset += chunkSize;
// Stop fetching if we have enough for this request (with buffer for filtering)
if (allTransactions.length >= offset + limit) {
break;
}
// Stop if we got less than requested (end of data)
if (chunk.transfers.length < chunkSize) {
hasMore = false;
}
}
let filteredTransactions = allTransactions.filter(tx => {
// Filter by timestamp range if provided
const type = sparkPaymentType(tx);
if (tx === 'sparl') return false;
if (from || until) {
const txTime = tx.createdTime
? new Date(tx.createdTime).getTime() / 1000
: null;
if (!txTime) return false;
if (from && txTime < from) return false;
if (until && txTime > until) return false;
}
// Filter by transaction type if specified
if (type) {
const isIncoming = tx.transferDirection === 'INCOMING';
const isOutgoing = tx.transferDirection === 'OUTGOING';
if (type === 'incoming' && !isIncoming) return false;
if (type === 'outgoing' && !isOutgoing) return false;
}
return true;
});
const paginatedTransactions = filteredTransactions.slice(
offset,
offset + limit,
);
const formatted = paginatedTransactions.map(tx => ({
type: tx.transferDirection?.toLowerCase() || 'unknown',
invoice: '',
description: '',
description_hash: null,
preimage: '',
payment_hash: '',
amount: tx.totalValue * 1000,
fees_paid: 0,
created_at: tx.createdTime
? Math.floor(new Date(tx.createdTime).getTime() / 1000)
: null,
settled_at: tx.expiryTime
? Math.floor(new Date(tx.updatedTime).getTime() / 1000)
: null,
metadata: {},
}));
return {
result_type: 'list_transactions',
result: {
transactions: formatted,
},
};
};
const handleMakeInvoice = async (
requestParams,
selectedNWCAccount,
@@ -151,6 +252,18 @@ const handleMakeInvoice = async (
});
const response = invoice.response;
NWCInvoiceManager.storeCreatedInvoice({
payment_hash: response.invoice.paymentHash,
invoice: response.invoice.encodedInvoice,
amount: response.invoice.amount.originalValue,
description: requestParams.description || '',
status: 'pending',
expires_at: response.invoice.expiresAt,
sparkID: response.id,
type: 'INCOMING',
fee: 0,
preimage: '',
});
return {
result_type: 'make_invoice',
result: {
@@ -169,11 +282,96 @@ const handleMakeInvoice = async (
};
};
const handleLookupInvoice = async requestParams => {
let foundInvoice;
try {
foundInvoice = await NWCInvoiceManager.handleLookupInvoice(requestParams);
} catch (err) {
console.log('Error handling lookup', err);
return createErrorResponse(
'lookup_invoice',
ERROR_CODES.INTERNAL,
err.message,
);
}
if (!foundInvoice) {
return createErrorResponse(
'lookup_invoice',
ERROR_CODES.INTERNAL,
'Unable to find invoice.',
);
}
const {sparkID, ...invoiceWithoutSparkID} = foundInvoice;
if (invoiceWithoutSparkID.status !== 'pending') {
return {
result_type: 'lookup_invoice',
result: invoiceWithoutSparkID,
};
}
const connectResponse = await ensureWalletConnection();
if (!connectResponse.isConnected) {
return createErrorResponse(
'lookup_invoice',
ERROR_CODES.INTERNAL,
'Unable to connect to wallet',
);
}
let sparkPaymentResponse;
if (invoiceWithoutSparkID.type === 'INCOMING') {
sparkPaymentResponse = await getNWCLightningReceiveRequest(sparkID);
} else {
sparkPaymentResponse = await NWCSparkLightningPaymentStatus(sparkID);
}
if (!sparkPaymentResponse.didWork)
return createErrorResponse(
'lookup_invoice',
ERROR_CODES.INTERNAL,
'Unable to lookup invoice.',
);
const data = sparkPaymentResponse.paymentResponse;
const status = getSparkPaymentStatus(data.status);
if (status !== 'pending') {
await NWCInvoiceManager.markInvoiceAsNotPending(
invoiceWithoutSparkID.payment_hash,
status,
data.paymentPreimage,
);
return {
result_type: 'lookup_invoice',
result: {
...invoiceWithoutSparkID,
status: status,
preimage: data.paymentPreimage || '',
settled_at: Date.now(),
},
};
}
return {
result_type: 'lookup_invoice',
result: invoiceWithoutSparkID,
};
};
const handlePayInvoice = async (
requestParams,
selectedNWCAccount,
fullStorageObject,
) => {
const hasAlreadyPaid = await NWCInvoiceManager.handleLookupInvoice({
invoice: requestParams.invoice,
});
if (hasAlreadyPaid) {
return createErrorResponse(
'pay_invoice',
ERROR_CODES.INTERNAL,
'Already paid this invoice.',
);
}
const connectResponse = await ensureWalletConnection();
const decoded = bolt11.decode(requestParams.invoice);
@@ -233,6 +431,20 @@ const handlePayInvoice = async (
await new Promise(res => setTimeout(res, 5000));
const status = await NWCSparkLightningPaymentStatus(response.id);
await NWCInvoiceManager.storeCreatedInvoice({
payment_hash: sha256Hash(status?.paymentResponse?.paymentPreimage || ''),
invoice: response.encodedInvoice,
amount: paymentAmount,
fee: Math.round(response.fee.originalValue / 1000),
description: '',
status: getSparkPaymentStatus(status?.paymentResponse.status),
created_at: response.createdAt,
sparkID: response.id,
type: 'OUTGOING',
preimage: status?.paymentResponse?.paymentPreimage || '',
});
if (!status.didWork) {
return createErrorResponse(
'pay_invoice',
@@ -380,6 +592,17 @@ const processEvent = async (event, selectedNWCAccount) => {
returnObject = handleGetInfo(selectedNWCAccount);
break;
case 'list_transactions':
if (!selectedNWCAccount.permissions.transactionHistory) {
returnObject = createErrorResponse(
requestMethod,
ERROR_CODES.RESTRICTED,
'Requested service is not authorized',
);
break;
}
returnObject = await handleGetTransactions(requestParams);
break;
case 'make_invoice':
if (!selectedNWCAccount.permissions.receivePayments) {
returnObject = createErrorResponse(
@@ -395,7 +618,17 @@ const processEvent = async (event, selectedNWCAccount) => {
fullStorageObject,
);
break;
case 'lookup_invoice':
if (!selectedNWCAccount.permissions.lookupInvoice) {
returnObject = createErrorResponse(
requestMethod,
ERROR_CODES.RESTRICTED,
'Requested service is not authorized',
);
break;
}
returnObject = await handleLookupInvoice(requestParams);
break;
case 'pay_invoice':
if (!selectedNWCAccount.permissions.sendPayments) {
returnObject = createErrorResponse(
@@ -446,15 +679,26 @@ export default async function handleNWCBackgroundEvent(notificationData) {
fullStorageObject = await getNWCData();
nwcAccounts = fullStorageObject.accounts;
const {
let {
data: {body: nwcEvent},
} = notificationData;
console.log('background nwc event', nwcEvent);
console.log(nwcAccounts);
if (!nwcEvent) return;
try {
nwcEvent = JSON.parse(nwcEvent);
} catch (err) {}
// // Filter out already handled events upfront
const newEvents = nwcEvent.events;
console.log('new NWC events', newEvents);
pushInstantNotification(
`Received ${newEvents.length} event${newEvents.length === 1 ? '' : 's'}`,
'Nostr Connect',
);
// nwcEvent.events.filter(event => {
// console.log(event, handledEventIds);
// if (handledEventIds.has(event.id)) return false;
@@ -470,6 +714,7 @@ export default async function handleNWCBackgroundEvent(notificationData) {
const eventPromises = newEvents.map(async (event, index) => {
const selectedNWCAccount = nwcAccounts[event.pubkey];
console.log(selectedNWCAccount, 'SELECTED NWC ACCOUNT');
if (!selectedNWCAccount) return null;
try {
+406
View File
@@ -0,0 +1,406 @@
import * as SQLite from 'expo-sqlite';
// Database configuration
const DB_NAME = 'nwc_invoices.db';
const DB_VERSION = 1;
class InvoiceDatabase {
constructor() {
this.db = null;
this.isInitialized = false;
}
// Initialize database connection
async initialize() {
try {
this.db = await SQLite.openDatabaseAsync(DB_NAME);
await this.createTables();
this.isInitialized = true;
console.log('Invoice database initialized successfully');
} catch (error) {
console.error('Failed to initialize database:', error);
throw error;
}
}
// Create necessary tables
async createTables() {
const createInvoicesTable = `
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
payment_hash TEXT NOT NULL UNIQUE,
invoice TEXT NOT NULL UNIQUE,
amount INTEGER,
description TEXT,
sparkID TEXT,
type TEXT,
status TEXT DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
expires_at INTEGER,
settled_at INTEGER,
metadata TEXT,
fee INTEGER,
preimage TEXT
);
`;
const createIndexes = `
CREATE INDEX IF NOT EXISTS idx_payment_hash ON invoices(payment_hash);
CREATE INDEX IF NOT EXISTS idx_invoice ON invoices(invoice);
CREATE INDEX IF NOT EXISTS idx_status ON invoices(status);
CREATE INDEX IF NOT EXISTS idx_created_at ON invoices(created_at);
`;
try {
await this.db.execAsync(createInvoicesTable);
await this.db.execAsync(createIndexes);
console.log('Database tables created successfully');
} catch (error) {
console.error('Failed to create tables:', error);
throw error;
}
}
// Ensure database is initialized
async ensureInitialized() {
if (!this.isInitialized) {
await this.initialize();
}
}
// Store a new invoice
async storeInvoice(invoiceData) {
await this.ensureInitialized();
const {
payment_hash,
invoice,
amount = null,
description = null,
expires_at = null,
settled_at = null,
metadata = null,
sparkID = null,
type = null,
fee,
preimage,
} = invoiceData;
const now = Date.now();
try {
const result = await this.db.runAsync(
`INSERT INTO invoices
(payment_hash, invoice, amount, description, created_at, updated_at, expires_at, settled_at, metadata, sparkID, type, fee, preimage)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
payment_hash,
invoice,
amount,
description,
now,
now,
expires_at,
settled_at,
metadata ? JSON.stringify(metadata) : null,
sparkID,
type,
fee,
preimage,
],
);
console.log('Invoice stored with ID:', result.lastInsertRowId);
return result.lastInsertRowId;
} catch (error) {
console.error('Failed to store invoice:', error);
throw error;
}
}
// Lookup invoice by invoice string
async lookupInvoiceByInvoiceString(invoiceString) {
await this.ensureInitialized();
try {
const result = await this.db.getFirstAsync(
'SELECT * FROM invoices WHERE invoice = ?',
[invoiceString],
);
if (result && result.metadata) {
try {
result.metadata = JSON.parse(result.metadata);
} catch (e) {
console.warn('Failed to parse metadata for invoice:', invoiceString);
}
}
return result;
} catch (error) {
console.error('Failed to lookup invoice by invoice string:', error);
throw error;
}
}
// Lookup invoice by payment hash
async lookupInvoiceByPaymentHash(paymentHash) {
await this.ensureInitialized();
try {
const result = await this.db.getFirstAsync(
'SELECT * FROM invoices WHERE payment_hash = ?',
[paymentHash],
);
if (result && result.metadata) {
try {
result.metadata = JSON.parse(result.metadata);
} catch (e) {
console.warn(
'Failed to parse metadata for payment hash:',
paymentHash,
);
}
}
return result;
} catch (error) {
console.error('Failed to lookup invoice by payment hash:', error);
throw error;
}
}
// Update invoice status
async updateInvoiceStatus(
paymentHash,
status,
settledAt = null,
preimage = '',
) {
await this.ensureInitialized();
const now = Date.now();
try {
const result = await this.db.runAsync(
`UPDATE invoices
SET status = ?, updated_at = ?, settled_at = ?, preimage = ?
WHERE payment_hash = ?`,
[status, now, settledAt, preimage, paymentHash],
);
return result.changes > 0;
} catch (error) {
console.error('Failed to update invoice status:', error);
throw error;
}
}
// Get all invoices with optional filtering
async getInvoices(filters = {}) {
await this.ensureInitialized();
let query = 'SELECT * FROM invoices';
const params = [];
const conditions = [];
if (filters.status) {
conditions.push('status = ?');
params.push(filters.status);
}
if (filters.limit) {
query += ' LIMIT ?';
params.push(filters.limit);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY created_at DESC';
try {
const results = await this.db.getAllAsync(query, params);
return results.map(result => {
if (result.metadata) {
try {
result.metadata = JSON.parse(result.metadata);
} catch (e) {
console.warn('Failed to parse metadata for invoice ID:', result.id);
}
}
return result;
});
} catch (error) {
console.error('Failed to get invoices:', error);
throw error;
}
}
// Delete old expired invoices
async cleanupExpiredInvoices() {
await this.ensureInitialized();
const now = Date.now();
try {
const result = await this.db.runAsync(
'DELETE FROM invoices WHERE expires_at IS NOT NULL AND expires_at < ? AND status = ?',
[now, 'pending'],
);
console.log('Cleaned up expired invoices:', result.changes);
return result.changes;
} catch (error) {
console.error('Failed to cleanup expired invoices:', error);
throw error;
}
}
async dropInvoicesTable() {
await this.ensureInitialized();
try {
await this.db.runAsync('DROP TABLE IF EXISTS invoices');
console.log('Invoices table dropped successfully');
return true;
} catch (error) {
console.error('Failed to drop invoices table:', error);
throw error;
}
}
async resetDatabase() {
await this.ensureInitialized();
try {
// Drop the table
await this.db.runAsync('DROP TABLE IF EXISTS invoices');
console.log('Dropped invoices table');
// Recreate the table
await this.createTables();
console.log('Database reset completed successfully');
return true;
} catch (error) {
console.error('Failed to reset database:', error);
throw error;
}
}
// Close database connection
async close() {
if (this.db) {
await this.db.closeAsync();
this.isInitialized = false;
console.log('Database connection closed');
}
}
}
const invoiceDB = new InvoiceDatabase();
// Utility functions for NWC integration
export const NWCInvoiceManager = {
// Initialize the database
async initialize() {
return await invoiceDB.initialize();
},
// Store invoice from create_invoice response
async storeCreatedInvoice(createInvoiceResponse) {
const {
payment_hash,
invoice,
amount,
description,
created_at,
settled_at,
expires_at,
sparkID,
type,
fee = 0,
preimage = '',
} = createInvoiceResponse;
return await invoiceDB.storeInvoice({
payment_hash,
invoice,
amount,
description,
created_at: created_at ? new Date(created_at).getTime() : null,
settled_at: settled_at ? new Date(settled_at).getTime() : null,
expires_at: expires_at ? new Date(expires_at).getTime() : null,
metadata: {created_via: 'nwc_create_invoice'},
sparkID,
type,
fee,
preimage,
});
},
// Handle lookup_invoice request
async handleLookupInvoice(request) {
const {payment_hash, invoice} = request;
if (!payment_hash && !invoice) {
throw new Error('Either payment_hash or invoice must be provided');
}
let result = null;
if (invoice) {
result = await invoiceDB.lookupInvoiceByInvoiceString(invoice);
} else if (payment_hash) {
result = await invoiceDB.lookupInvoiceByPaymentHash(payment_hash);
}
if (!result) {
return null; // Invoice not found
}
// Return in NWC format
return {
type: result.type,
invoice: result.invoice,
description: result.description,
preimage: result.preimage,
payment_hash: result.payment_hash,
amount: result.amount,
fees_paid: result.fee,
created_at: result.created_at,
expires_at: result.expires_at,
settled_at: result.settled_at,
status: result.status,
sparkID: result.sparkID,
};
},
// Update invoice when payment is received
async markInvoiceAsNotPending(paymentHash, status, preimgae) {
return await invoiceDB.updateInvoiceStatus(
paymentHash,
status,
Date.now(),
preimgae,
);
},
async dropTable() {
return await invoiceDB.dropInvoicesTable();
},
async resetDatabase() {
return await invoiceDB.resetDatabase();
},
// Get database instance for direct access
getDatabase() {
return invoiceDB;
},
};
export default NWCInvoiceManager;
+6
View File
@@ -29,6 +29,12 @@ export function getSupportedMethods(accountPermissions) {
if (accountPermissions.getBalance) {
supportedCommands.push('get_balance');
}
if (accountPermissions.transactionHistory) {
supportedCommands.push('list_transactions');
}
if (accountPermissions.lookupInvoice) {
supportedCommands.push('lookup_invoice');
}
supportedCommands.push('get_info');
return supportedCommands;
+1
View File
@@ -1,4 +1,5 @@
import {SimplePool} from 'nostr-tools';
import {pushInstantNotification} from '../notifications';
// Configuration
const RELAY_TIMEOUT = 10000; // 5 seconds timeout per message
+13 -1
View File
@@ -101,7 +101,7 @@ export const sendNWCSparkLightningPayment = async ({
if (!nwcWallet) throw new Error('sparkWallet not initialized');
const paymentResponse = await nwcWallet.payLightningInvoice({
invoice,
maxFeeSats: maxFeeSats,
maxFeeSats: Math.round(maxFeeSats * 1.2),
amountSatsToSend: amountSats,
});
return {didWork: true, paymentResponse};
@@ -120,6 +120,18 @@ export const NWCSparkLightningPaymentStatus = async id => {
return {didWork: false, error: err.message};
}
};
export const getNWCLightningReceiveRequest = async lightningInvoiceId => {
try {
if (!nwcWallet) throw new Error('nwcWallet not initialized');
const paymentResponse = await nwcWallet.getLightningReceiveRequest(
lightningInvoiceId,
);
return {didWork: true, paymentResponse};
} catch (err) {
console.log('Get lightning payment status error', err);
return {didWork: false, error: err.message};
}
};
export const getNWCSparkTransactions = async (
transferCount = 100,
offsetIndex,
+1 -1
View File
@@ -394,7 +394,7 @@ async function processLightningTransaction(
matchResult.matchedUnpaidInvoice?.invoice?.encodedInvoice || '',
preimage: matchResult.matchedUnpaidInvoice?.paymentPreimage || '',
shouldNavigate: matchResult.savedInvoice?.shouldNavigate ?? 0,
isLNULR: savedDetails?.isLNURL || false,
isLNURL: savedDetails?.isLNURL || false,
},
};
}
+1 -1
View File
@@ -42,7 +42,7 @@ export async function transformTxToPaymentObject(
? JSON.parse(foundInvoice.details)?.isBlitzContactPayment
: undefined,
shouldNavigate: foundInvoice ? foundInvoice?.shouldNavigate : undefined,
isLNULR: foundInvoice
isLNURL: foundInvoice
? JSON.parse(foundInvoice.details)?.isLNURL
: undefined,
},
+14 -14
View File
@@ -1,17 +1,17 @@
import {useState} from 'react';
import {useEffect} from 'react';
import {AppState} from 'react-native';
// import {useState} from 'react';
// import {useEffect} from 'react';
// import {AppState} from 'react-native';
export const useIsForeground = () => {
const [isForeground, setIsForeground] = useState(true);
// export const useIsForeground = () => {
// const [isForeground, setIsForeground] = useState(true);
useEffect(() => {
const onChange = state => {
setIsForeground(state === 'active');
};
const listener = AppState.addEventListener('change', onChange);
return () => listener.remove();
}, [setIsForeground]);
// useEffect(() => {
// const onChange = state => {
// setIsForeground(state === 'active');
// };
// const listener = AppState.addEventListener('change', onChange);
// return () => listener.remove();
// }, [setIsForeground]);
return isForeground;
};
// return isForeground;
// };
+3 -2
View File
@@ -37,6 +37,7 @@ import useHandleBackPressNew from '../../hooks/useHandleBackPressNew';
import {useCallback} from 'react';
// import {keyboardGoBack} from '../../functions/customNavigation';
import ExploreUsers from './explorePage';
import NostrHome from '../../components/admin/homeComponents/settingsContent/nostrHome';
export default function SettingsContentIndex(props) {
const navigate = useNavigation();
@@ -147,8 +148,8 @@ export default function SettingsContentIndex(props) {
)}
{selectedPage?.toLowerCase() === 'blitz stats' && <ExploreUsers />}
{selectedPage?.toLowerCase() === 'noster connect' && (
<NosterWalletConnect theme={theme} />
{selectedPage?.toLowerCase() === 'nostr' && (
<NostrHome theme={theme} />
)}
{selectedPage?.toLowerCase() === 'login mode' && (
<LoginSecurity extraData={extraData} theme={theme} />
+7 -7
View File
@@ -119,13 +119,13 @@ const EXPIRIMENTALFEATURES = [
},
];
const ADVANCEDOPTIONS = [
// {
// for: 'general',
// name: 'Noster Connect',
// svgIcon: true,
// svgName: 'linkIcon',
// arrowIcon: ICONS.leftCheveronIcon,
// },
{
for: 'general',
name: 'Nostr',
svgIcon: true,
svgName: 'linkIcon',
arrowIcon: ICONS.leftCheveronIcon,
},
{
for: 'Closing Account',
name: 'Blitz Fee Details',
+94 -29
View File
@@ -5,10 +5,13 @@ import {
useEffect,
useMemo,
useCallback,
useRef,
} from 'react';
import {AppState} from 'react-native';
import {getBoltzSwapPairInformation} from '../app/functions/boltz/boltzSwapInfo';
import * as Network from 'expo-network';
import {navigationRef} from '../navigation/navigationService';
// Initiate context
const AppStatusManager = createContext(null);
@@ -21,20 +24,50 @@ const AppStatusProvider = ({children}) => {
max: 10000000,
},
});
const [isConnectedToTheInternet, setIsConnectedToTheInternet] =
useState(null);
const [didGetToHomepage, setDidGetToHomePage] = useState(false);
const [appState, setAppState] = useState(AppState.currentState);
const hasInitializedNavListener = useRef(false);
const hasInitializedBoltzData = useRef(false);
const hasInitializedNetworkMonitoring = useRef(false);
const toggleDidGetToHomepage = useCallback(newInfo => {
setDidGetToHomePage(newInfo);
}, []);
const toggleMinMaxLiquidSwapAmounts = useCallback(newInfo => {
setMinMaxLiquidSwapAmounts(prev => ({...prev, ...newInfo}));
}, []);
useEffect(() => {
const handleAppStateChange = nextAppState => {
console.log('App state changed to:', nextAppState);
setAppState(nextAppState);
};
const subscription = AppState.addEventListener(
'change',
handleAppStateChange,
);
return () => {
subscription?.remove();
};
}, []);
useEffect(() => {
if (appState !== 'active' || hasInitializedNavListener.current) {
if (appState !== 'active') {
console.log('Skipping navigation listener setup - app not active');
}
return;
}
console.log('Setting up navigation listener - first time app is active');
hasInitializedNavListener.current = true;
const unsubscribe = navigationRef.addListener('state', () => {
console.log(
'Current navigation stack',
@@ -45,35 +78,59 @@ const AppStatusProvider = ({children}) => {
return () => {
unsubscribe();
};
}, []);
}, [appState]);
useEffect(() => {
if (appState !== 'active' || hasInitializedBoltzData.current) {
if (appState !== 'active') {
console.log('Skipping Boltz API calls - app not active');
}
return;
}
console.log('Making Boltz API calls - first time app is active');
hasInitializedBoltzData.current = true;
(async () => {
const [submarineSwapStats, reverseSwapStats] = await Promise.all([
getBoltzSwapPairInformation('submarine'),
getBoltzSwapPairInformation('reverse'),
]);
try {
const [submarineSwapStats, reverseSwapStats] = await Promise.all([
getBoltzSwapPairInformation('submarine'),
getBoltzSwapPairInformation('reverse'),
]);
const liquidReverse = reverseSwapStats.BTC['L-BTC'];
const liquidReverse = reverseSwapStats.BTC['L-BTC'];
const min = liquidReverse?.limits?.minimal || 1000;
const max = liquidReverse?.limits?.maximal || 25000000;
const min = liquidReverse?.limits?.minimal || 1000;
const max = liquidReverse?.limits?.maximal || 25000000;
toggleMinMaxLiquidSwapAmounts({
reverseSwapStats: liquidReverse,
submarineSwapStats: submarineSwapStats['L-BTC'].BTC,
min,
max,
rsk: {
submarine: reverseSwapStats.BTC.RBTC,
reverse: submarineSwapStats.RBTC.BTC,
min: reverseSwapStats.BTC.RBTC.limits.minimal,
max: reverseSwapStats.BTC.RBTC.limits.maximal,
},
});
toggleMinMaxLiquidSwapAmounts({
reverseSwapStats: liquidReverse,
submarineSwapStats: submarineSwapStats['L-BTC'].BTC,
min,
max,
rsk: {
submarine: reverseSwapStats.BTC.RBTC,
reverse: submarineSwapStats.RBTC.BTC,
min: reverseSwapStats.BTC.RBTC.limits.minimal,
max: reverseSwapStats.BTC.RBTC.limits.maximal,
},
});
} catch (error) {
console.error('Error fetching Boltz swap information:', error);
}
})();
}, []);
}, [appState, toggleMinMaxLiquidSwapAmounts]);
useEffect(() => {
if (appState !== 'active' || hasInitializedNetworkMonitoring.current) {
if (appState !== 'active') {
console.log('Skipping network monitoring setup - app not active');
}
return;
}
console.log('Setting up network monitoring - first time app is active');
hasInitializedNetworkMonitoring.current = true;
const networkSubscription = Network.addNetworkStateListener(
({type, isConnected, isInternetReachable}) => {
console.log(
@@ -84,15 +141,21 @@ const AppStatusProvider = ({children}) => {
);
const checkNetworkState = async () => {
const networkState = await Network.getNetworkStateAsync();
console.log(networkState, 'network state in startup function');
setIsConnectedToTheInternet(networkState.isConnected);
try {
const networkState = await Network.getNetworkStateAsync();
console.log(networkState, 'network state in startup function');
setIsConnectedToTheInternet(networkState.isConnected);
} catch (error) {
console.error('Error checking network state:', error);
}
};
checkNetworkState();
return () => networkSubscription.remove();
}, []);
return () => {
networkSubscription.remove();
};
}, [appState]);
console.log(minMaxLiquidSwapAmounts, 'min max liquid swap amounts');
@@ -103,6 +166,7 @@ const AppStatusProvider = ({children}) => {
isConnectedToTheInternet,
didGetToHomepage,
toggleDidGetToHomepage,
appState,
}),
[
minMaxLiquidSwapAmounts,
@@ -110,6 +174,7 @@ const AppStatusProvider = ({children}) => {
isConnectedToTheInternet,
didGetToHomepage,
toggleDidGetToHomepage,
appState,
],
);
+15 -5
View File
@@ -8,6 +8,7 @@ import {
getMessaging,
isDeviceRegisteredForRemoteMessages,
registerDeviceForRemoteMessages,
setBackgroundMessageHandler,
} from '@react-native-firebase/messaging';
import {encriptMessage} from '../app/functions/messaging/encodingAndDecodingMessages';
import {useGlobalContextProvider} from './context';
@@ -173,22 +174,25 @@ async function registerForPushNotificationsAsync() {
throw new Error(
'Google Play Services are required to receive notifications.',
);
console.log('Registering notification channel on android');
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync(
'blitzWalletNotifications',
{
name: 'blitzWalletNotifications',
importance: Notifications.AndroidImportance.MAX,
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
showBadge: true,
bypassDnd: false,
},
);
}
if (DeviceInfo.isEmulatorSync()) {
throw new Error('Must use physical device for Push Notifications');
}
// if (DeviceInfo.isEmulatorSync()) {
// throw new Error('Must use physical device for Push Notifications');
// }
const permissionsResult = await Notifications.getPermissionsAsync();
let finalStatus = permissionsResult.status;
@@ -235,7 +239,13 @@ TaskManager.defineTask(BACKGROUND_NOTIFICATION_TASK, async ({data, error}) => {
export async function registerBackgroundNotificationTask() {
try {
await Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK);
if (Platform.OS === 'android') {
setBackgroundMessageHandler(firebaseMessaging, async data => {
await handleNWCBackgroundEvent(data);
});
} else {
await Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK);
}
} catch (error) {
console.error('Task registration failed:', error);
}
+70 -53
View File
@@ -53,7 +53,7 @@ const SparkWalletManager = createContext(null);
const sessionTime = new Date().getTime();
const SparkWalletProvider = ({children}) => {
const {accountMnemoinc, contactsPrivateKey, publicKey} = useKeysContext();
const {didGetToHomepage, minMaxLiquidSwapAmounts} = useAppStatus();
const {didGetToHomepage, minMaxLiquidSwapAmounts, appState} = useAppStatus();
const {liquidNodeInformation} = useNodeContext();
const [isSendingPayment, setIsSendingPayment] = useState(false);
const {toggleGlobalContactsInformation, globalContactsInformation} =
@@ -74,8 +74,8 @@ const SparkWalletProvider = ({children}) => {
const updatePendingPaymentsIntervalRef = useRef(null);
const isInitialRestore = useRef(true);
const didInitializeSendingPaymentEvent = useRef(false);
const initialBitcoinIntervalRun = useRef(null);
const [numberOfCachedTxs, setNumberOfCachedTxs] = useState(0);
const [currentAppState, setCurrentAppState] = useState('');
// Debounce refs
const debounceTimeoutRef = useRef(null);
@@ -178,9 +178,9 @@ const SparkWalletProvider = ({children}) => {
);
const details = JSON.parse(selectedStoredPayment.details);
if (details?.shouldNavigate && !details.isLNULR) return;
if (details?.shouldNavigate && !details.isLNURL) return;
if (
details.isLNULR &&
details.isLNURL &&
!details.isBlitzContactPayment &&
navigationRef
.getRootState()
@@ -316,38 +316,43 @@ const SparkWalletProvider = ({children}) => {
}, 500);
};
const addListeners = async () => {
const addListeners = async mode => {
console.log('Adding Spark listeners...');
if (AppState.currentState !== 'active') return;
sparkTransactionsEventEmitter.removeAllListeners(
SPARK_TX_UPDATE_ENVENT_NAME,
);
sparkTransactionsEventEmitter.on(SPARK_TX_UPDATE_ENVENT_NAME, handleUpdate);
sparkWallet.on('transfer:claimed', transferHandler);
// sparkWallet.on('deposit:confirmed', transferHandler);
if (isInitialRestore.current) {
isInitialRestore.current = false;
}
if (mode === 'full') {
sparkWallet.on('transfer:claimed', transferHandler);
await fullRestoreSparkState({
sparkAddress: sparkInformation.sparkAddress,
batchSize: isInitialRestore.current ? 15 : 5,
savedTxs: sparkInformation.transactions,
isSendingPayment: isSendingPayment,
});
await updateSparkTxStatus();
if (updatePendingPaymentsIntervalRef.current) {
console.log('BLOCKING TRYING TO SET INTERVAL AGAIN');
return;
}
updatePendingPaymentsIntervalRef.current = setInterval(async () => {
try {
await updateSparkTxStatus();
} catch (err) {
console.error('Error during periodic restore:', err);
if (isInitialRestore.current) {
isInitialRestore.current = false;
}
}, 10 * 1000);
await fullRestoreSparkState({
sparkAddress: sparkInformation.sparkAddress,
batchSize: isInitialRestore.current ? 15 : 5,
savedTxs: sparkInformation.transactions,
isSendingPayment: isSendingPayment,
});
await updateSparkTxStatus();
if (updatePendingPaymentsIntervalRef.current) {
console.log('BLOCKING TRYING TO SET INTERVAL AGAIN');
clearInterval(updatePendingPaymentsIntervalRef.current);
}
updatePendingPaymentsIntervalRef.current = setInterval(async () => {
try {
await updateSparkTxStatus();
} catch (err) {
console.error('Error during periodic restore:', err);
}
}, 10 * 1000);
}
};
const removeListeners = () => {
@@ -360,10 +365,16 @@ const SparkWalletProvider = ({children}) => {
sparkWallet.listenerCount('transfer:claimed'),
'number of spark wallet listenre',
);
sparkTransactionsEventEmitter.removeAllListeners(
SPARK_TX_UPDATE_ENVENT_NAME,
);
sparkWallet?.removeAllListeners('transfer:claimed');
if (
sparkTransactionsEventEmitter.listenerCount(SPARK_TX_UPDATE_ENVENT_NAME)
) {
sparkTransactionsEventEmitter.removeAllListeners(
SPARK_TX_UPDATE_ENVENT_NAME,
);
}
if (sparkWallet.listenerCount('transfer:claimed')) {
sparkWallet?.removeAllListeners('transfer:claimed');
}
// sparkWallet?.removeAllListeners('deposit:confirmed');
// Clear debounce timeout when removing listeners
@@ -381,28 +392,27 @@ const SparkWalletProvider = ({children}) => {
}
};
// Add event listeners to listen for bitcoin and lightning or spark transfers when receiving does not handle sending
useEffect(() => {
if (!currentAppState) return;
if (currentAppState === 'active') {
addListeners();
} else if (currentAppState.match(/inactive|background/)) {
removeListeners();
}
}, [currentAppState]);
// Add event listeners to listen for bitcoin and lightning or spark transfers when receiving only when screen is active
useEffect(() => {
if (!didGetToHomepage) return;
if (!sparkInformation.didConnect) return;
const handleAppStateChange = nextAppState => {
setCurrentAppState(nextAppState);
};
AppState.addEventListener('change', handleAppStateChange);
// Add on mount if app is already active
if (AppState.currentState === 'active') {
setCurrentAppState('active');
const shouldHaveListeners = appState === 'active' && !isSendingPayment;
const shouldHaveSparkEventEmitter = appState === 'active';
if (shouldHaveListeners) {
addListeners('full');
} else if (shouldHaveSparkEventEmitter && isSendingPayment) {
addListeners('sparkOnly');
} else {
removeListeners();
}
}, [sparkInformation.didConnect, didGetToHomepage]);
}, [
appState,
sparkInformation.didConnect,
didGetToHomepage,
isSendingPayment,
]);
useEffect(() => {
if (!didGetToHomepage) return;
@@ -412,6 +422,8 @@ const SparkWalletProvider = ({children}) => {
const handleDepositAddressCheck = async () => {
try {
console.log('l1Deposit check running....');
console.log(AppState.currentState);
if (AppState.currentState !== 'active') return;
const allTxs = await getAllSparkTransactions();
const savedTxMap = new Map(allTxs.map(tx => [tx.sparkID, tx]));
const depoistAddress = await queryAllStaticDepositAddresses();
@@ -579,13 +591,18 @@ const SparkWalletProvider = ({children}) => {
clearInterval(depositAddressIntervalRef.current);
}
setTimeout(handleDepositAddressCheck, 1_000 * 5);
if (isSendingPayment) return;
if (!initialBitcoinIntervalRun.current) {
setTimeout(handleDepositAddressCheck, 1_000 * 5);
initialBitcoinIntervalRun.current = true;
}
depositAddressIntervalRef.current = setInterval(
handleDepositAddressCheck,
1_000 * 60,
);
}, [sparkInformation.didConnect, didGetToHomepage]);
}, [sparkInformation.didConnect, didGetToHomepage, isSendingPayment]);
// This function connects to the spark node and sets the session up
+4 -4
View File
@@ -67,10 +67,10 @@ export const WebViewProvider = ({children}) => {
}
console.log('Setting up claim retry interval');
handleClaimRetryRef.current = setInterval(
handleUnclaimedReverseSwaps,
30000,
);
// handleClaimRetryRef.current = setInterval(
// handleUnclaimedReverseSwaps,
// 30000,
// );
}, [isWEbViewReady, didGetToHomepage]);
return (
+53
View File
@@ -17,6 +17,7 @@ import {
writeBatch,
or,
orderBy,
deleteDoc,
} from '@react-native-firebase/firestore';
import {getLocalStorageItem, setLocalStorageItem} from '../app/functions';
import {
@@ -419,3 +420,55 @@ function processWithRAF(allMessages, myPubKey, privateKey) {
requestAnimationFrame(processChunk);
});
}
export async function isValidNip5Name(wantedName) {
try {
crashlyticsLogReport('Seeing if the unique name exists');
const usersRef = collection(db, 'nip5Verification');
const q = query(
usersRef,
where('nameLower', '==', wantedName.toLowerCase()),
);
const querySnapshot = await getDocs(q);
return querySnapshot.empty;
} catch (error) {
console.error('Error checking unique name:', error);
crashlyticsRecordErrorReport(error.message);
return false;
}
}
export async function addNip5toCollection(dataObject, uuid) {
try {
if (!uuid) throw Error('Not authenticated');
crashlyticsLogReport('Starting to add data to nip5');
const db = getFirestore();
const docRef = doc(db, 'nip5Verification', uuid);
await setDoc(docRef, dataObject, {merge: true});
return true;
} catch (e) {
console.error('Error adding document: ', e);
crashlyticsRecordErrorReport(e.message);
return false;
}
}
export async function deleteNip5FromCollection(uuid) {
try {
if (!uuid) throw Error('Not authenticated');
crashlyticsLogReport('Starting to add data to collection');
const db = getFirestore();
const docRef = doc(db, 'nip5Verification', uuid);
await deleteDoc(docRef);
console.log('Document deleted');
return true;
} catch (e) {
console.error('Error deleting document', e);
crashlyticsRecordErrorReport(e.message);
return false;
}
}
+4
View File
@@ -36,6 +36,7 @@ import {
ConfirmActionPage,
HistoricalOnChainPayments,
LspDescriptionPopup,
NosterWalletConnect,
TotalTipsScreen,
ViewPOSTransactions,
} from '../app/components/admin/homeComponents/settingsContent';
@@ -44,6 +45,7 @@ import {
// import RestoreProofsPopup from '../app/components/admin/homeComponents/settingsContent/experimentalComponents/restoreProofsPopup';
import RefundLiquidSwapPopup from '../app/components/admin/homeComponents/settingsContent/failedLiquidSwapsComponents/refundSwapPopup';
import ConfirmPinForLoginMode from '../app/components/admin/homeComponents/settingsContent/loginSecurity/enterPinPage';
import Nip5VerificationPage from '../app/components/admin/homeComponents/settingsContent/nip5/nip5Account';
import CreateNostrConnectAccount from '../app/components/admin/homeComponents/settingsContent/nwc/createNWCAccount';
import NWCWallet from '../app/components/admin/homeComponents/settingsContent/nwc/NWCWalletPage';
import NWCWalletSetup from '../app/components/admin/homeComponents/settingsContent/nwc/showSeedPage';
@@ -125,9 +127,11 @@ const SLIDE_FROM_RIGHT_SCREENS = [
{name: 'RestoreWallet', component: RestoreWallet},
// {name: 'EcashSettings', component: EcashSettings},
{name: 'AddPOSItemsPage', component: AddPOSItemsPage},
{name: 'NosterWalletConnect', component: NosterWalletConnect},
{name: 'CreateNostrConnectAccount', component: CreateNostrConnectAccount},
{name: 'NWCWallet', component: NWCWallet},
{name: 'NWCWalletSetup', component: NWCWalletSetup},
{name: 'Nip5VerificationPage', component: Nip5VerificationPage},
];
const FADE_SCREENS = [