mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
v0.1.8-beta - push notifications, claim ecash in batch, Wallet address recovery tool when moving to new device.
This commit is contained in:
@@ -91,6 +91,7 @@ jobs:
|
||||
echo NATIVE_VERSION_ANDROID='${{ env.ANDROID_VERSION_NAME }}' >> .env
|
||||
echo JS_BUNDLE_VERSION='${{ env.CURRENT_JS_VERSION }}' >> .env
|
||||
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
|
||||
echo MINIBIT_SERVER_NOSTR_PUBKEY='${{ secrets.MINIBIT_SERVER_NOSTR_PUBKEY }}' >> .env
|
||||
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
|
||||
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash' >> .env
|
||||
|
||||
@@ -91,6 +91,7 @@ jobs:
|
||||
echo NATIVE_VERSION_ANDROID='${{ env.ANDROID_VERSION_NAME }}' >> .env
|
||||
echo JS_BUNDLE_VERSION='${{ env.CURRENT_JS_VERSION }}' >> .env
|
||||
echo MINIBITS_SERVER_API_KEY='${{ secrets.MINIBITS_SERVER_API_KEY }}' >> .env
|
||||
echo MINIBIT_SERVER_NOSTR_PUBKEY='${{ secrets.MINIBIT_SERVER_NOSTR_PUBKEY }}' >> .env
|
||||
echo MINIBITS_SERVER_API_HOST='https://api.minibits.cash' >> .env
|
||||
echo MINIBITS_NIP05_DOMAIN='@minibits.cash' >> .env
|
||||
echo MINIBITS_RELAY_URL='wss://relay.minibits.cash/test' >> .env
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.google.gms.google-services"
|
||||
// apply plugin: "kotlin-android" // minibits:react-native-camera-kit
|
||||
// apply plugin: "kotlin-android-extensions"
|
||||
|
||||
@@ -102,8 +103,8 @@ android {
|
||||
applicationId "com.minibits_wallet"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 52001
|
||||
versionName "0.1.7"
|
||||
versionCode 55001
|
||||
versionName "0.1.8"
|
||||
}
|
||||
|
||||
splits {
|
||||
|
||||
@@ -20,6 +20,7 @@ buildscript {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
classpath("com.google.gms:google-services:4.4.1")
|
||||
// classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") // minibits_wallet:react-native-camera-kit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,19 @@ import Crypto from 'react-native-quick-crypto' // needed for secp256k1, conf in
|
||||
import 'react-native-url-polyfill/auto' // URL.host etc
|
||||
import 'text-encoding-polyfill' // needed in cashu-ts
|
||||
import {AppRegistry} from 'react-native'
|
||||
import messaging from '@react-native-firebase/messaging';
|
||||
import App from './src/App'
|
||||
import {name as appName} from './app.json'
|
||||
import { NotificationService } from './src/services/notificationService';
|
||||
|
||||
|
||||
function BootstrapApp() {
|
||||
return <App appName={appName} />
|
||||
}
|
||||
|
||||
|
||||
// Setup notification listeners and handlers
|
||||
messaging().onMessage(NotificationService.onReceiveRemoteNotification)
|
||||
messaging().setBackgroundMessageHandler(NotificationService.onReceiveRemoteNotification)
|
||||
|
||||
AppRegistry.registerComponent(appName, () => BootstrapApp)
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.1.7-beta.17",
|
||||
"version": "0.1.8-beta.15",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
@@ -26,6 +26,8 @@
|
||||
"@notifee/react-native": "^7.8.2",
|
||||
"@react-native-clipboard/clipboard": "^1.13.2",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-native-firebase/app": "^20.0.0",
|
||||
"@react-native-firebase/messaging": "^20.0.0",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/native-stack": "^6.9.26",
|
||||
|
||||
+21
-5
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import {
|
||||
APP_ENV,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
|
||||
} from '@env'
|
||||
import codePush from 'react-native-code-push'
|
||||
import messaging from '@react-native-firebase/messaging'
|
||||
import FlashMessage from "react-native-flash-message"
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
@@ -20,12 +21,13 @@ import {
|
||||
} from '@gocodingnow/rn-size-matters'
|
||||
import {AppNavigator} from './navigation'
|
||||
import {useInitialRootStore, useStores} from './models'
|
||||
import {Database} from './services'
|
||||
import {Database, MinibitsClient} from './services'
|
||||
import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary'
|
||||
import Config from './config'
|
||||
import {log} from './services'
|
||||
import {Env} from './utils/envtypes'
|
||||
import AppError from './utils/AppError'
|
||||
|
||||
// RN 0.73 screen rendering issue
|
||||
//import { enableFreeze, enableScreens } from 'react-native-screens';
|
||||
// enableScreens(false)
|
||||
@@ -55,10 +57,10 @@ interface AppProps {
|
||||
}
|
||||
|
||||
function App(props: AppProps) {
|
||||
const {userSettingsStore, relaysStore} = useStores()
|
||||
const {rehydrated} = useInitialRootStore(() => {
|
||||
const {userSettingsStore, relaysStore, walletProfileStore} = useStores()
|
||||
|
||||
const {rehydrated} = useInitialRootStore(async() => {
|
||||
// This runs after the root store has been initialized and rehydrated from storage.
|
||||
|
||||
|
||||
// Creates and opens a sqlite database that stores transactions history and user settings.
|
||||
// It triggers db migrations if database version has changed.
|
||||
@@ -73,8 +75,22 @@ function App(props: AppProps) {
|
||||
for (const relay of relaysStore.allRelays) {
|
||||
relay.setStatus(WebSocket.CLOSED)
|
||||
}
|
||||
|
||||
// FCM push notifications - set or refresh token on app start
|
||||
await messaging().registerDeviceForRemoteMessages()
|
||||
const deviceToken = await messaging().getToken()
|
||||
|
||||
log.trace('[useInitialRootStore]', {deviceToken})
|
||||
|
||||
// Save new or refreshed token to local and server profile
|
||||
if (deviceToken !== walletProfileStore.device) {
|
||||
walletProfileStore.setDevice(deviceToken)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
if (!rehydrated) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { StyleSheet, View, ActivityIndicator, ViewProps, TextStyle } from 'react
|
||||
import { useThemeColor } from '../theme'
|
||||
import { Text } from './Text'
|
||||
|
||||
export function Loading(props: ViewProps & {statusMessage?: string, textStyle: TextStyle}) {
|
||||
export function Loading(props: ViewProps & {statusMessage?: string, textStyle?: TextStyle}) {
|
||||
return (
|
||||
<View style={[{
|
||||
flex: 1,
|
||||
|
||||
+3
-3
@@ -265,9 +265,9 @@ const en = {
|
||||
backupScreen: {
|
||||
localBackup: "Local backup",
|
||||
localBackupDescription: "Local backup stores a copy of all your ecash in the local database. Ecash is never deleted unless you switch off the backup. Please note that this backup is not encrypted.",
|
||||
remoteBackup: "Off-device backup",
|
||||
remoteBackupDescription: "Seed phrase allows you to recover your ecash balance in case of device loss.",
|
||||
recoveryTool: "Recovery tool",
|
||||
remoteBackup: "Seed backup",
|
||||
remoteBackupDescription: "Mnemonic phrase encodes your seed and allows you to recover your ecash balance in case of device loss.",
|
||||
recoveryTool: "Local recovery tool",
|
||||
recoveryToolDescription: "Show ecash backed up in the local database and attempt to recover unspent ones into the wallet in case the wallet storage gets corrupted or a transaction fails due to an unexpected error.",
|
||||
removeSpentCoins: "Remove spent ecash",
|
||||
removeSpentCoinsDescription: "In case of a SEND or TRANSFER transaction error, spent ecash may remain in your wallet. This blocks further transactions with the mint, making the wallet unusable. This tool removes spent ecash from the wallet."
|
||||
|
||||
@@ -256,6 +256,10 @@ export const WalletProfileStoreModel = types
|
||||
log.info('[recover]', 'Wallet profile recovered in WalletProfileStore', {self, publishedEvent})
|
||||
return self
|
||||
}),
|
||||
setDevice: flow(function* setDevice(device: string) {
|
||||
self.device = device
|
||||
yield MinibitsClient.updateDeviceToken(self.pubkey, {deviceToken: device})
|
||||
}),
|
||||
setNip05(nip05: string) { // used in migration to v3 model
|
||||
self.nip05 = nip05
|
||||
},
|
||||
@@ -264,7 +268,8 @@ export const WalletProfileStoreModel = types
|
||||
},
|
||||
setSeedHash(seedHash: string) { // used in migration to v8 model
|
||||
self.seedHash = seedHash
|
||||
}
|
||||
},
|
||||
|
||||
/* setPicture(picture: string) {
|
||||
self.picture = picture
|
||||
}*/
|
||||
|
||||
@@ -22,7 +22,7 @@ import {Database} from '../../services'
|
||||
import { log } from '../../services/logService'
|
||||
import { rootStoreModelVersion } from '../RootStore'
|
||||
import AppError, { Err } from '../../utils/AppError'
|
||||
import { MINIBITS_NIP05_DOMAIN } from '@env'
|
||||
import { MINIBITS_NIP05_DOMAIN, MINIBITS_RELAY_URL } from '@env'
|
||||
import { LogLevel } from '../../services/log/logTypes'
|
||||
import { MintStatus } from '../Mint'
|
||||
|
||||
@@ -133,41 +133,6 @@ async function _runMigrations(rootStore: RootStore) {
|
||||
let currentVersion = rootStore.version
|
||||
|
||||
try {
|
||||
if(currentVersion < 3) {
|
||||
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v3`)
|
||||
if(walletProfileStore.pubkey) {
|
||||
walletProfileStore.setNip05(walletProfileStore.name+MINIBITS_NIP05_DOMAIN)
|
||||
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
|
||||
rootStore.setVersion(rootStoreModelVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(currentVersion < 4) {
|
||||
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v4`)
|
||||
if(walletProfileStore.pubkey) {
|
||||
walletProfileStore.setWalletId(userSettingsStore.walletId as string)
|
||||
|
||||
// publish profile to relays
|
||||
await walletProfileStore.publishToRelays()
|
||||
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
|
||||
rootStore.setVersion(rootStoreModelVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(currentVersion < 5) {
|
||||
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v5`)
|
||||
if(contactsStore.publicRelay) {
|
||||
relaysStore.addRelay({
|
||||
url: contactsStore.publicRelay,
|
||||
status: WebSocket.CLOSED
|
||||
})
|
||||
}
|
||||
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
|
||||
rootStore.setVersion(rootStoreModelVersion)
|
||||
}
|
||||
|
||||
|
||||
if(currentVersion < 6) {
|
||||
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v6`)
|
||||
@@ -288,14 +253,16 @@ async function _runMigrations(rootStore: RootStore) {
|
||||
|
||||
if(currentVersion < 14) {
|
||||
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v14`)
|
||||
try {
|
||||
try {
|
||||
|
||||
await walletProfileStore.migrateToNewRelay()
|
||||
|
||||
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
|
||||
rootStore.setVersion(rootStoreModelVersion)
|
||||
} catch (e: any) {
|
||||
log.warn('[setupRootStore]', e.message)
|
||||
log.warn('[setupRootStore] Migration error', {message: e.name})
|
||||
}
|
||||
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
|
||||
rootStore.setVersion(rootStoreModelVersion)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
|
||||
@@ -40,7 +40,7 @@ import { colors, useThemeColor } from "../theme"
|
||||
*/
|
||||
export type AppStackParamList = {
|
||||
Welcome: undefined
|
||||
RemoteRecovery: undefined
|
||||
RemoteRecovery: {isAddressOnlyRecovery: boolean}
|
||||
Mints: {}
|
||||
Tabs: NavigatorScreenParams<TabsParamList>
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
ReceiveOption,
|
||||
SendOption,
|
||||
TokenReceiveScreen,
|
||||
RemoteRecoveryScreen,
|
||||
} from "../screens"
|
||||
import { Contact } from "../models/Contact"
|
||||
import { colors, useThemeColor, spacing, typography } from "../theme"
|
||||
@@ -288,6 +289,7 @@ export type SettingsStackParamList = {
|
||||
LocalRecovery: undefined
|
||||
Developer: undefined
|
||||
Relays: undefined
|
||||
RemoteRecovery: {isAddressOnlyRecovery: boolean}
|
||||
WalletNavigator: {screen: string}
|
||||
ContactsNavigator: {screen: string}
|
||||
}
|
||||
@@ -319,6 +321,7 @@ const SettingsNavigator = function SettingsNavigator() {
|
||||
<SettingsStack.Screen name="LocalRecovery" component={LocalRecoveryScreen} />
|
||||
<SettingsStack.Screen name="Developer" component={DeveloperScreen} />
|
||||
<SettingsStack.Screen name="Relays" component={RelaysScreen} />
|
||||
<SettingsStack.Screen name="RemoteRecovery" component={RemoteRecoveryScreen} />
|
||||
</SettingsStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,11 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
|
||||
const gotoRemoteBackup = function () {
|
||||
navigation.navigate('RemoteBackup')
|
||||
}
|
||||
}
|
||||
|
||||
const gotoRemoteRecovery = function () {
|
||||
navigation.navigate('RemoteRecovery', {isAddressOnlyRecovery: true})
|
||||
}
|
||||
|
||||
const checkSpent = async function () {
|
||||
setIsLoading(true)
|
||||
@@ -157,7 +161,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
<ListItem
|
||||
tx="backupScreen.remoteBackup"
|
||||
subTx="backupScreen.remoteBackupDescription"
|
||||
leftIcon='faUpRightFromSquare'
|
||||
@@ -165,7 +169,17 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={gotoRemoteBackup}
|
||||
/>
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
text="Wallet address recovery"
|
||||
subText="Use your mnemonic phrase from another device to get your original wallet address on this device. This will not recover balances."
|
||||
leftIcon='faCircleUser'
|
||||
leftIconColor={colors.palette.iconGreyBlue400}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
onPress={gotoRemoteRecovery}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -238,7 +252,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
|
||||
<>
|
||||
<ListItem
|
||||
text="Increase recovery indexes"
|
||||
subText={`After migration from old wallet to the wallet that can be recovered with seed phrase, you may in rare cases encounter 'duplicate outputs' error when trying to send. This resets your recovery indexes to higher value in order to resolve the issue.`}
|
||||
subText={`After incomplete recovery from seed or other rare issue, you may encounter 'duplicate outputs' error when trying to send. This resets your recovery indexes to higher value in order to resolve the issue.`}
|
||||
leftIcon='faArrowUp'
|
||||
leftIconColor={colors.palette.success300}
|
||||
leftIconInverse={true}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> =
|
||||
return (
|
||||
<Screen style={$screen}>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Text preset="heading" text="Off-device backup" style={{color: 'white'}} />
|
||||
<Text preset="heading" text="Seed backup" style={{color: 'white'}} />
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
@@ -108,7 +108,7 @@ export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> =
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Your mnemonic phrase'
|
||||
subText='This 12 word sequence allows you to recover your ecash balance in case of device loss. Keep them in safe place.'
|
||||
subText='This 12 word sequence represents your seed and allows you to recover your ecash balance in case of device loss. Keep them in safe place.'
|
||||
leftIcon='faInfoCircle'
|
||||
leftIconColor={colors.palette.iconYellow300}
|
||||
leftIconInverse={true}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useEffect, useRef, useState} from 'react'
|
||||
import {FlatList, LayoutAnimation, Platform, Pressable, Switch, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native'
|
||||
import {validateMnemonic} from '@scure/bip39'
|
||||
import QuickCrypto from 'react-native-quick-crypto'
|
||||
import { wordlist } from '@scure/bip39/wordlists/english'
|
||||
import {colors, spacing, useThemeColor} from '../theme'
|
||||
import {AppStackScreenProps} from '../navigation' // @demo remove-current-line
|
||||
@@ -42,6 +43,7 @@ import { WalletUtils } from '../services/wallet/utils'
|
||||
import { WalletScreen } from './WalletScreen'
|
||||
import { MintUnit, formatCurrency, getCurrency } from '../services/wallet/currency'
|
||||
import { isObj } from '@cashu/cashu-ts/src/utils'
|
||||
import { WalletProfileRecord } from '../models/WalletProfileStore'
|
||||
|
||||
if (Platform.OS === 'android' &&
|
||||
UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
@@ -64,10 +66,12 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
const indexInputRef = useRef<TextInput>(null)
|
||||
|
||||
const [info, setInfo] = useState('')
|
||||
const [isAddressOnlyRecovery, setIsAddressOnlyRecovery] = useState(route.params.isAddressOnlyRecovery ? true : false)
|
||||
const [mnemonic, setMnemonic] = useState<string>('')
|
||||
const [mnemonicExists, setMnemonicExists] = useState(false)
|
||||
const [isValidMnemonic, setIsValidMnemonic] = useState(false)
|
||||
const [seed, setSeed] = useState<Uint8Array>()
|
||||
const [profileToRecover, setProfileToRecover] = useState<WalletProfileRecord | undefined>(undefined)
|
||||
const [selectedMintUrl, setSelectedMintUrl] = useState<string | undefined>()
|
||||
const [selectedKeyset, setSelectedKeyset] = useState<MintKeyset | undefined>()
|
||||
const [selectedMintKeysets, setSelectedMintKeysets] = useState<MintKeyset[]>([])
|
||||
@@ -166,7 +170,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
|
||||
|
||||
const onAddMints = function (): void {
|
||||
return navigation.navigate('Mints')
|
||||
return navigation.navigate('Mints', {})
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +477,75 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
|
||||
}
|
||||
|
||||
const onFindWalletAddress = async () => {
|
||||
try {
|
||||
if(!seed || !mnemonic) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic or seed.')
|
||||
}
|
||||
|
||||
const seedHash = QuickCrypto.createHash('sha256')
|
||||
.update(seed)
|
||||
.digest('hex')
|
||||
|
||||
const currentSeedHash = await KeyChain.loadSeedHash()
|
||||
|
||||
if(currentSeedHash === seedHash) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Can not use seed from this device.')
|
||||
}
|
||||
|
||||
const profile = await MinibitsClient.getWalletProfileBySeedHash(seedHash as string)
|
||||
|
||||
// Skip external profiles beacause we do not control keys
|
||||
if(profile) {
|
||||
log.info('[onCheckWalletAddress] profileToRecover', {profile})
|
||||
|
||||
if(profile.nip05.includes(MINIBITS_NIP05_DOMAIN)) {
|
||||
setProfileToRecover(profile)
|
||||
} else {
|
||||
setInfo(`You used wallet address ${profile.nip05} with your own keys, import it again.`)
|
||||
await delay(5000)
|
||||
}
|
||||
} else {
|
||||
setInfo('Could not find wallet address linked to your seed on the server.')
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onCompleteAddress = async () => {
|
||||
try {
|
||||
if(!seed || !mnemonic || !profileToRecover) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mnemonic or seed or profile to recover.')
|
||||
}
|
||||
|
||||
setStatusMessage('Recovering wallet address...')
|
||||
setIsLoading(true)
|
||||
|
||||
await KeyChain.saveMnemonic(mnemonic)
|
||||
await KeyChain.saveSeed(seed as Uint8Array)
|
||||
const seedHash = await KeyChain.loadSeedHash()
|
||||
|
||||
const {publicKey: newPublicKey} = await NostrClient.getOrCreateKeyPair()
|
||||
// Updates pubkey and imports wallet profile
|
||||
await walletProfileStore.recover(seedHash as string, newPublicKey)
|
||||
// Align walletId in userSettings with recovered profile
|
||||
userSettingsStore.setWalletId(walletProfileStore.walletId)
|
||||
await delay(1000)
|
||||
setStatusMessage(`Recovery completed`)
|
||||
await delay(2000)
|
||||
|
||||
|
||||
userSettingsStore.setIsOnboarded(true)
|
||||
setStatusMessage('')
|
||||
setIsLoading(false)
|
||||
navigation.navigate('Tabs')
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onComplete = async () => {
|
||||
try {
|
||||
@@ -539,7 +612,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
</View>
|
||||
|
||||
<View style={$contentContainer}>
|
||||
{mnemonicExists ? (
|
||||
{mnemonicExists && !isAddressOnlyRecovery ? (
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
@@ -565,7 +638,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
) : (
|
||||
<>
|
||||
{isValidMnemonic ? (
|
||||
<>
|
||||
<>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
@@ -577,121 +650,158 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = o
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text='Recovery from mints'
|
||||
subText='Identify mints to recover your ecash from and add them to the list.'
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
|
||||
RightComponent={mintsStore.mintCount > 0 ? (
|
||||
<View style={$rightContainer}>
|
||||
{isAddressOnlyRecovery ? (
|
||||
<>
|
||||
{profileToRecover ? (
|
||||
<>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text={profileToRecover.nip05}
|
||||
subText={'This is the wallet address linked to your seed. If you press Complete, your wallet profile will reset to above seed and address.'}
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
|
||||
style={$item}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={onAddMints}
|
||||
text='Mints'
|
||||
preset='secondary'
|
||||
/>
|
||||
onPress={onCompleteAddress}
|
||||
text={'Complete recovery'}
|
||||
style={{marginRight: spacing.small}}
|
||||
/>
|
||||
</View>
|
||||
) : (undefined)
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
{mintsStore.mintCount === 0 && (
|
||||
</>
|
||||
) : (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={onAddMints}
|
||||
text='Add mints'
|
||||
/>
|
||||
onPress={onFindWalletAddress}
|
||||
text={'Find wallet address'}
|
||||
style={{marginRight: spacing.small}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ContentComponent={
|
||||
<>
|
||||
{mintsStore.mints.map((mint: Mint, index: number) => (
|
||||
<MintListItem
|
||||
key={mint.mintUrl}
|
||||
mint={mint}
|
||||
mintBalance={proofsStore.getMintBalance(mint.mintUrl)}
|
||||
onMintSelect={() => onMintSelect(mint)}
|
||||
isSelectable={true}
|
||||
isSelected={selectedMintUrl === mint.mintUrl}
|
||||
separator={index === 0 ? 'both' : 'bottom'}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<>
|
||||
{mintsStore.mintCount > 0 && selectedMintUrl && (
|
||||
<>
|
||||
<View style={$buttonContainer}>
|
||||
{(startIndex > 0 || totalRecoveredAmount > 0) && (
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text='Recovery from mints'
|
||||
subText='Identify mints to recover your ecash from and add them to the list.'
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
|
||||
RightComponent={mintsStore.mintCount > 0 ? (
|
||||
<View style={$rightContainer}>
|
||||
<Button
|
||||
onPress={onAddMints}
|
||||
text='Mints'
|
||||
preset='secondary'
|
||||
/>
|
||||
</View>
|
||||
) : (undefined)
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
{mintsStore.mintCount === 0 && (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={onComplete}
|
||||
text={'Complete'}
|
||||
style={{marginRight: spacing.small}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={startRecovery}
|
||||
text={startIndex === 0 ? 'Start recovery' : 'Next interval'}
|
||||
preset={(startIndex === 0 || totalRecoveredAmount > 0) ? 'default' : 'secondary'}
|
||||
disabled={selectedMintUrl ? false : true}
|
||||
/>
|
||||
</View>
|
||||
onPress={onAddMints}
|
||||
text='Add mints'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ContentComponent={
|
||||
<>
|
||||
{mintsStore.mints.map((mint: Mint, index: number) => (
|
||||
<MintListItem
|
||||
key={mint.mintUrl}
|
||||
mint={mint}
|
||||
mintBalance={proofsStore.getMintBalance(mint.mintUrl)}
|
||||
onMintSelect={() => onMintSelect(mint)}
|
||||
isSelectable={true}
|
||||
isSelected={selectedMintUrl === mint.mintUrl}
|
||||
separator={index === 0 ? 'both' : 'bottom'}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
FooterComponent={
|
||||
<>
|
||||
{mintsStore.mintCount > 0 && selectedMintUrl && (
|
||||
<>
|
||||
<View style={$buttonContainer}>
|
||||
{(startIndex > 0 || totalRecoveredAmount > 0) && (
|
||||
<Button
|
||||
onPress={onComplete}
|
||||
text={'Complete'}
|
||||
style={{marginRight: spacing.small}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={startRecovery}
|
||||
text={startIndex === 0 ? 'Start recovery' : 'Next interval'}
|
||||
preset={(startIndex === 0 || totalRecoveredAmount > 0) ? 'default' : 'secondary'}
|
||||
disabled={selectedMintUrl ? false : true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={$buttonContainer}>
|
||||
<Text
|
||||
text={`Recovery interval ${startIndex} - ${endIndex} · `}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
|
||||
/>
|
||||
<Pressable onPress={toggleIndexModal}>
|
||||
<Text
|
||||
text={`Set manually`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={[$buttonContainer,{marginTop: 0}]}>
|
||||
<Text
|
||||
text={`Keyset ID ${selectedKeyset?.id} (${selectedKeyset?.unit})`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.extraSmall}}
|
||||
/>
|
||||
{selectedMintKeysets.length > 1 && (
|
||||
<Pressable onPress={toggleKeysetModal}>
|
||||
<View style={$buttonContainer}>
|
||||
<Text
|
||||
text={` · Select another`}
|
||||
text={`Recovery interval ${startIndex} - ${endIndex} · `}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
|
||||
/>
|
||||
<Pressable onPress={toggleIndexModal}>
|
||||
<Text
|
||||
text={`Set manually`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={[$buttonContainer,{marginTop: 0}]}>
|
||||
<Text
|
||||
text={`Keyset ID ${selectedKeyset?.id} (${selectedKeyset?.unit})`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.extraSmall}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{mintsStore.mintCount > 0 && !selectedMintUrl && (
|
||||
<Text
|
||||
text={`Select mint to recover from`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', margin: spacing.large}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
/>
|
||||
{selectedMintKeysets.length > 1 && (
|
||||
<Pressable onPress={toggleKeysetModal}>
|
||||
<Text
|
||||
text={` · Select another`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', marginTop: spacing.extraSmall}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{mintsStore.mintCount > 0 && !selectedMintUrl && (
|
||||
<Text
|
||||
text={`Select mint to recover from`}
|
||||
size='xxs'
|
||||
style={{color: textHint, alignSelf: 'center', margin: spacing.large}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text='Insert backup mnemonic phrase'
|
||||
subText='Paste or rewrite 12 word phrase to recover your ecash balance on this device. Separate words by blank spaces.'
|
||||
text='Insert mnemonic phrase'
|
||||
subText={`Paste or rewrite 12 word phrase to recover your ${isAddressOnlyRecovery ? 'wallet address' : 'ecash balance and wallet address'} on this device. Separate words by blank spaces.`}
|
||||
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
|
||||
style={$item}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {FC, useCallback, useEffect, useState} from 'react'
|
||||
import {Alert, FlatList, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import React, {FC, useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Alert, AppState, FlatList, Switch, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import notifee from '@notifee/react-native'
|
||||
import {
|
||||
APP_ENV,
|
||||
CODEPUSH_STAGING_DEPLOYMENT_KEY,
|
||||
@@ -18,6 +19,7 @@ import {Env} from '../utils/envtypes'
|
||||
import { round } from '../utils/number'
|
||||
import { getCurrency } from '../services/wallet/currency'
|
||||
import { getMintColor } from './WalletScreen'
|
||||
import { NotificationService } from '../services/notificationService'
|
||||
|
||||
|
||||
interface SettingsScreenProps extends SettingsStackScreenProps<'Settings'> {}
|
||||
@@ -28,12 +30,14 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
function SettingsScreen(_props) {
|
||||
const {navigation} = _props
|
||||
useHeader({}) // default header component
|
||||
const appState = useRef(AppState.currentState)
|
||||
const {mintsStore, relaysStore, userSettingsStore} = useStores()
|
||||
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
|
||||
const [updateDescription, setUpdateDescription] = useState<string>('')
|
||||
const [updateSize, setUpdateSize] = useState<string>('')
|
||||
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState<boolean>(false)
|
||||
const [areNotificationsEnabled, setAreNotificationsEnabled] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkForUpdate = async () => {
|
||||
@@ -54,6 +58,43 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const getNotificationPermission = async () => {
|
||||
try {
|
||||
const enabled = await NotificationService.areNotificationsEnabled()
|
||||
setAreNotificationsEnabled(enabled)
|
||||
} catch (e: any) {
|
||||
log.info(e.name, e.message)
|
||||
return false // silent
|
||||
}
|
||||
}
|
||||
getNotificationPermission()
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', async(nextAppState) => {
|
||||
if (
|
||||
appState.current.match(/inactive|background/) &&
|
||||
nextAppState === 'active') {
|
||||
try {
|
||||
const enabled = await NotificationService.areNotificationsEnabled()
|
||||
setAreNotificationsEnabled(enabled)
|
||||
} catch (e: any) {
|
||||
log.info(e.name, e.message)
|
||||
return false // silent
|
||||
}
|
||||
}
|
||||
|
||||
appState.current = nextAppState
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.remove()
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
const handleBinaryVersionMismatchCallback = function(update: RemotePackage) {
|
||||
// silent
|
||||
// setIsNativeUpdateAvailable(true)
|
||||
@@ -92,6 +133,10 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
})
|
||||
}
|
||||
|
||||
const openNotificationSettings = async function() {
|
||||
await notifee.openNotificationSettings()
|
||||
}
|
||||
|
||||
const gotoPreferredUnit = function() {
|
||||
Alert.alert('Preferred unit is set based on your Wallet screen.')
|
||||
}
|
||||
@@ -143,10 +188,58 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
text={getCurrency(userSettingsStore.preferredUnit).code}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
bottomSeparator={true}
|
||||
}
|
||||
bottomSeparator={false}
|
||||
onPress={gotoPreferredUnit}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={[$card, {marginTop: spacing.large}]}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text='Push notifications'
|
||||
leftIcon='faPaperPlane'
|
||||
leftIconColor={colors.palette.green400}
|
||||
leftIconInverse={true}
|
||||
style={$item}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Text
|
||||
style={$itemRight}
|
||||
text={areNotificationsEnabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
bottomSeparator={true}
|
||||
onPress={openNotificationSettings}
|
||||
/>
|
||||
<ListItem
|
||||
text={'Nostr relays'}
|
||||
subText={`Connected: ${relaysStore.connectedCount}`}
|
||||
leftIcon='faCircleNodes'
|
||||
leftIconColor={colors.palette.iconViolet200}
|
||||
leftIconInverse={true}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Text
|
||||
style={$itemRight}
|
||||
text={`${relaysStore.allRelays.length} relays`}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
onPress={gotoRelays}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={[$card, {marginTop: spacing.large}]}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
tx='settingsScreen.backupRecovery'
|
||||
leftIcon='faCloudArrowUp'
|
||||
@@ -202,28 +295,7 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={[$card, {marginTop: spacing.large}]}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
text={'Nostr relays'}
|
||||
subText={`Connected: ${relaysStore.connectedCount}`}
|
||||
leftIcon='faCircleNodes'
|
||||
leftIconColor={colors.palette.iconViolet200}
|
||||
leftIconInverse={true}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Text
|
||||
style={$itemRight}
|
||||
text={`${relaysStore.allRelays.length} relays`}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
onPress={gotoRelays}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
</View>
|
||||
</Screen>
|
||||
)
|
||||
@@ -231,7 +303,7 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
|
||||
)
|
||||
|
||||
const $screen: ViewStyle = {
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
|
||||
+106
-17
@@ -18,6 +18,7 @@ import {
|
||||
import codePush, { RemotePackage } from 'react-native-code-push'
|
||||
import {moderateVerticalScale, verticalScale} from '@gocodingnow/rn-size-matters'
|
||||
import { SvgXml } from 'react-native-svg'
|
||||
import {getUnixTime} from 'date-fns'
|
||||
import PagerView, { PagerViewOnPageScrollEventData } from 'react-native-pager-view'
|
||||
import { ScalingDot } from 'react-native-animated-pagination-dots'
|
||||
import {useThemeColor, spacing, colors, typography} from '../theme'
|
||||
@@ -92,15 +93,19 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
const [currentUnit, setCurrentUnit] = useState<MintUnit>(groupedMints.length > 0 ? groupedMints[0].unit : 'sat')
|
||||
const [info, setInfo] = useState<string>('')
|
||||
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
|
||||
const [lastClaimCheck, setLastClaimCheck] = useState<number>(getUnixTime(new Date()))
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
|
||||
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false)
|
||||
const [isSendModalVisible, setIsSendModalVisible] = useState<boolean>(false)
|
||||
const [isReceiveModalVisible, setIsReceiveModalVisible] = useState<boolean>(false)
|
||||
const [updateDescription, setUpdateDescription] = useState<string>('')
|
||||
const [updateSize, setUpdateSize] = useState<string>('')
|
||||
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState<boolean>(false)
|
||||
|
||||
// On app start
|
||||
useEffect(() => {
|
||||
const checkForUpdate = async () => {
|
||||
try {
|
||||
@@ -136,21 +141,22 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
// toggleUpdateModal()
|
||||
}
|
||||
|
||||
|
||||
// On app start
|
||||
useEffect(() => {
|
||||
// get deeplink if any
|
||||
const getInitialData = async () => {
|
||||
const getInitialData = async () => {
|
||||
const url = await Linking.getInitialURL()
|
||||
|
||||
if (url) {
|
||||
handleDeeplink({url})
|
||||
return // deeplinks have priority over clipboard
|
||||
return // skip further processing so that it does not slow down or clash deep link
|
||||
}
|
||||
|
||||
if(!isInternetReachable) { return }
|
||||
|
||||
// Auto-recover inflight proofs - do only on startup and before checkPendingReceived to prevent conflicts
|
||||
// TODO add manual option to recovery settings
|
||||
// check lnaddress claims on app start and set timestamp to trigger focus updates
|
||||
WalletTask.handleClaim().catch(e => false)
|
||||
// Auto-recover inflight proofs - do only on startup and before checkPendingReceived to prevent conflicts
|
||||
WalletTask.handleInFlight().catch(e => false)
|
||||
// Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs
|
||||
WalletTask.receiveEventsFromRelays().catch(e => false)
|
||||
@@ -197,7 +203,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}, initial: false})
|
||||
}
|
||||
|
||||
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if(!isInternetReachable) {
|
||||
@@ -205,7 +211,18 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}
|
||||
|
||||
WalletTask.handleSpentFromPending().catch(e => false)
|
||||
WalletTask.handlePendingTopups().catch(e => false)
|
||||
WalletTask.handlePendingTopups().catch(e => false)
|
||||
|
||||
// check lnaddress claims max once per minute to decrease server load
|
||||
const nowInSec = getUnixTime(new Date())
|
||||
|
||||
log.trace('[useFocusEffect]', {nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined})
|
||||
|
||||
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
|
||||
WalletTask.handleClaim().catch(e => false)
|
||||
setLastClaimCheck(nowInSec)
|
||||
}
|
||||
|
||||
}, [])
|
||||
)
|
||||
|
||||
@@ -235,6 +252,17 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
|
||||
WalletTask.handleSpentFromPending().catch(e => false)
|
||||
WalletTask.handlePendingTopups().catch(e => false)
|
||||
|
||||
// check lnaddress claims max once per minute to decrease server load
|
||||
const nowInSec = getUnixTime(new Date())
|
||||
|
||||
log.trace('[appState change]', {nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined})
|
||||
|
||||
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
|
||||
WalletTask.handleClaim().catch(e => false)
|
||||
setLastClaimCheck(nowInSec)
|
||||
}
|
||||
|
||||
// calls checkPendingReceived if re-connects
|
||||
NostrClient.reconnectToRelays().catch(e => false)
|
||||
}
|
||||
@@ -253,6 +281,15 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}
|
||||
|
||||
|
||||
const toggleSendModal = () => {
|
||||
setIsSendModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
|
||||
const toggleReceiveModal = () => {
|
||||
setIsReceiveModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
const addMint = async function ({scannedMintUrl = ''} = {}) {
|
||||
// necessary
|
||||
navigation.setParams({scannedMintUrl: undefined})
|
||||
@@ -285,22 +322,30 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}
|
||||
}
|
||||
|
||||
const gotoTokenReceive = async function () {
|
||||
/* const routes = navigation.getState()?.routes
|
||||
const state = navigation.getState()
|
||||
log.trace('[gotoTokenReceive]', {routes, state}) */
|
||||
|
||||
navigation.navigate('TokenReceive', {unit: currentUnit})
|
||||
const gotoSend = function () {
|
||||
toggleSendModal()
|
||||
navigation.navigate('Send', {unit: currentUnit})
|
||||
}
|
||||
|
||||
const gotoSend = function () {
|
||||
navigation.navigate('Send', {unit: currentUnit})
|
||||
const gotoLightningPay = async function () {
|
||||
toggleSendModal()
|
||||
navigation.navigate('LightningPay', {unit: currentUnit})
|
||||
}
|
||||
|
||||
const gotoScan = function () {
|
||||
navigation.navigate('Scan')
|
||||
}
|
||||
|
||||
const gotoTokenReceive = async function () {
|
||||
toggleReceiveModal()
|
||||
navigation.navigate('TokenReceive', {unit: currentUnit})
|
||||
}
|
||||
|
||||
const gotoTopup = function () {
|
||||
toggleReceiveModal()
|
||||
navigation.navigate('Topup', {unit: currentUnit})
|
||||
}
|
||||
|
||||
const gotoTranDetail = function (id: number) {
|
||||
// navigation.navigate('TranDetail', {id})
|
||||
navigation.navigate('TransactionsNavigator', {screen: 'TranDetail', params: {id}, initial: false})
|
||||
@@ -482,7 +527,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
//style={{paddingLeft: spacing.medium}}
|
||||
/>
|
||||
)}
|
||||
onPress={gotoSend}
|
||||
onPress={toggleSendModal}
|
||||
style={[{backgroundColor: mainButtonColor, borderWidth: 1, borderColor: screenBg}, $buttonTopup]}
|
||||
preset='tertiary'
|
||||
text='Send'
|
||||
@@ -508,7 +553,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
color={mainButtonIcon}
|
||||
/>
|
||||
)}
|
||||
onPress={gotoTokenReceive}
|
||||
onPress={toggleReceiveModal}
|
||||
text='Receive'
|
||||
style={[{backgroundColor: mainButtonColor, borderWidth: 1, borderColor: screenBg}, $buttonPay]}
|
||||
preset='tertiary'
|
||||
@@ -538,6 +583,50 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
|
||||
}
|
||||
onBackButtonPress={toggleUpdateModal}
|
||||
onBackdropPress={toggleUpdateModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isSendModalVisible ? true : false}
|
||||
style={{alignItems: 'stretch'}}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
leftIcon='faMoneyBill1'
|
||||
text='Send Ecash'
|
||||
subText='Share Ecash or send it to one of your contacts'
|
||||
onPress={gotoSend}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon='faBolt'
|
||||
text='Pay with Lightning'
|
||||
subText='Pay invoice or to a Lightning address'
|
||||
onPress={gotoLightningPay}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleSendModal}
|
||||
onBackdropPress={toggleSendModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isReceiveModalVisible ? true : false}
|
||||
style={{alignItems: 'stretch'}}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
leftIcon='faMoneyBill1'
|
||||
text='Receive Ecash'
|
||||
subText='Paste or scan Ecash token'
|
||||
onPress={gotoTokenReceive}
|
||||
/>
|
||||
<ListItem
|
||||
leftIcon='faBolt'
|
||||
text='Topup with Lightning'
|
||||
subText='Create Lightning invoice to topup your balance'
|
||||
onPress={gotoTopup}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleReceiveModal}
|
||||
onBackdropPress={toggleReceiveModal}
|
||||
/>
|
||||
|
||||
</Screen>
|
||||
|
||||
@@ -104,7 +104,7 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
|
||||
|
||||
const gotoRecovery = async function () {
|
||||
try {
|
||||
navigation.navigate('RemoteRecovery')
|
||||
navigation.navigate('RemoteRecovery', {isAddressOnlyRecovery: false})
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ const saveSeed = async function (
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads mnemonic from the KeyChain/KeyStore
|
||||
* Loads seed from the KeyChain/KeyStore
|
||||
*
|
||||
*/
|
||||
const loadSeed = async function (): Promise<Uint8Array | undefined> {
|
||||
|
||||
@@ -8,6 +8,8 @@ import * as Sentry from '@sentry/react-native'
|
||||
import {rootStoreInstance} from '../models'
|
||||
import {LogLevel} from './log/logTypes'
|
||||
|
||||
// refresh
|
||||
|
||||
const {
|
||||
userSettingsStore
|
||||
} = rootStoreInstance
|
||||
|
||||
@@ -99,6 +99,27 @@ const updateWalletProfileNip05 = async function (pubkey: string, update: {newPub
|
||||
}
|
||||
|
||||
|
||||
|
||||
const updateDeviceToken = async function (pubkey: string, update: {deviceToken: string} ) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'PUT'
|
||||
const { deviceToken } = update
|
||||
|
||||
const body = {
|
||||
deviceToken
|
||||
}
|
||||
|
||||
const walletProfile: WalletProfile = await fetchApi(url + `/deviceToken/${pubkey}`, {
|
||||
method,
|
||||
body,
|
||||
})
|
||||
|
||||
log.info('[updateDeviceToken]', `Updated wallet deviceToken`, walletProfile.device)
|
||||
|
||||
return walletProfile
|
||||
}
|
||||
|
||||
|
||||
const recoverProfile = async function (seedHash: string, update: {newPubkey: string}) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'PUT'
|
||||
@@ -213,7 +234,7 @@ const createDonation = async function (amount: number, memo: string, pubkey: str
|
||||
body
|
||||
})
|
||||
|
||||
log.info(`Created new donation invoice`, invoice, 'createDonation')
|
||||
log.info(`[createDonation] Created new donation invoice`, {invoice})
|
||||
|
||||
return invoice
|
||||
}
|
||||
@@ -227,14 +248,35 @@ const checkDonationPaid = async function (paymentHash: string, pubkey: string) {
|
||||
method,
|
||||
})
|
||||
|
||||
log.info(`Got response`, donationPaid, 'checkDonationPaid')
|
||||
log.info(`[checkDonationPaid] Got response`, donationPaid)
|
||||
|
||||
return donationPaid
|
||||
}
|
||||
|
||||
|
||||
const createClaim = async function (walletId: string, seedHash: string, pubkey: string,) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/claim'
|
||||
const method = 'POST'
|
||||
|
||||
const body = {
|
||||
walletId,
|
||||
seedHash,
|
||||
pubkey
|
||||
}
|
||||
|
||||
const claimedInvoices: Array<{token: string, }> = await fetchApi(url, {
|
||||
method,
|
||||
body
|
||||
})
|
||||
|
||||
log.info(`[createClaim] Created new claim and got invoices`, {claimedInvoices})
|
||||
|
||||
return claimedInvoices
|
||||
}
|
||||
|
||||
|
||||
const fetchApi = async (url: string, options: MinibitsRequestOptions, timeout = 10000) => { //ms
|
||||
log.trace('[fetchApi]', {url})
|
||||
log.trace('[fetchApi] start', url)
|
||||
|
||||
const controller = new AbortController()
|
||||
const body = options.body ? JSON.stringify(options.body) : undefined
|
||||
@@ -290,6 +332,7 @@ export const MinibitsClient = {
|
||||
createWalletProfile,
|
||||
updateWalletProfile,
|
||||
updateWalletProfileNip05,
|
||||
updateDeviceToken,
|
||||
recoverProfile,
|
||||
migrateSeedHash,
|
||||
getRandomPictures,
|
||||
@@ -298,6 +341,7 @@ export const MinibitsClient = {
|
||||
getWalletProfileBySeedHash,
|
||||
createDonation,
|
||||
checkDonationPaid,
|
||||
createClaim,
|
||||
getPublicHeaders,
|
||||
fetchApi,
|
||||
}
|
||||
@@ -80,8 +80,7 @@ const reconnectToRelays = async function () {
|
||||
|
||||
// recreate subscriptions if all relays down
|
||||
if(relaysStore.connectedCount === 0) {
|
||||
WalletTask.handleSpentFromPending().catch(e => false)
|
||||
WalletTask.handleSpentFromPending().catch(e => false)
|
||||
WalletTask.handleSpentFromPending().catch(e => false)
|
||||
}
|
||||
|
||||
const pool = getRelayPool()
|
||||
|
||||
@@ -1,19 +1,73 @@
|
||||
import notifee, { AndroidImportance, AuthorizationStatus } from '@notifee/react-native'
|
||||
import { colors } from '../theme';
|
||||
import { log } from './logService';
|
||||
import {
|
||||
MINIBIT_SERVER_NOSTR_PUBKEY,
|
||||
} from '@env'
|
||||
import { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
|
||||
import { MintUnit, formatCurrency, getCurrency } from './wallet/currency';
|
||||
import { NostrClient, NostrProfile } from './nostrService';
|
||||
import AppError, { Err } from '../utils/AppError';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export type RemoteMessageReceiveToLnurl = {
|
||||
type: 'RemoteMessageReceiveToLnurl',
|
||||
data: {
|
||||
amount: number,
|
||||
unit: 'sat',
|
||||
comment?: string | null,
|
||||
zapSenderProfile?: NostrProfile
|
||||
}
|
||||
}
|
||||
|
||||
// Remote notification receive handler
|
||||
const onReceiveRemoteNotification = async function(remoteMessage: FirebaseMessagingTypes.RemoteMessage) {
|
||||
log.debug('[onReceiveRemoteNotification]', {remoteMessage})
|
||||
try {
|
||||
|
||||
const {encrypted} = remoteMessage.data!
|
||||
|
||||
if (!encrypted) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Unknown remote message data received', {data: remoteMessage.data})
|
||||
}
|
||||
|
||||
const serverPubkey = MINIBIT_SERVER_NOSTR_PUBKEY
|
||||
|
||||
const decrypted = await NostrClient.decryptNip04(serverPubkey, encrypted as string)
|
||||
|
||||
if (decrypted) {
|
||||
const remoteData: RemoteMessageReceiveToLnurl = JSON.parse(decrypted)
|
||||
const {amount, unit, comment, zapSenderProfile} = remoteData.data
|
||||
|
||||
const currencyCode = getCurrency(unit as MintUnit).code
|
||||
|
||||
await createLocalNotification(
|
||||
`<b>⚡${formatCurrency(amount, currencyCode)} ${currencyCode}</b> incoming!`,
|
||||
`${zapSenderProfile ? 'Zap' : 'Payment'} from <b>${zapSenderProfile?.nip05 || 'unknown payer'}</b> is ready to be received.${comment ? ' Message from sender: ' + comment : ''}`,
|
||||
zapSenderProfile?.picture
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error(e.name, e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Local notification creation
|
||||
const createLocalNotification = async function (title: string, body: string, largeIcon?: string) {
|
||||
log.trace('Start', {title, body, largeIcon}, 'createLocalNotification')
|
||||
// Request permissions (required for iOS)
|
||||
await notifee.requestPermission()
|
||||
if(Platform.OS === 'ios') {
|
||||
await notifee.requestPermission()
|
||||
}
|
||||
|
||||
// Create a channel (required for Android)
|
||||
const channelId = await notifee.createChannel({
|
||||
id: 'important',
|
||||
name: 'Minibits payment notifications',
|
||||
vibration: true,
|
||||
importance: AndroidImportance.HIGH,
|
||||
id: 'default',
|
||||
name: 'Minibits notifications',
|
||||
vibration: true,
|
||||
importance: AndroidImportance.HIGH,
|
||||
})
|
||||
|
||||
// Display a notification
|
||||
@@ -24,7 +78,10 @@ const createLocalNotification = async function (title: string, body: string, lar
|
||||
android: {
|
||||
channelId,
|
||||
color: colors.palette.success200,
|
||||
largeIcon
|
||||
largeIcon,
|
||||
pressAction: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -35,7 +92,10 @@ const createLocalNotification = async function (title: string, body: string, lar
|
||||
body,
|
||||
android: {
|
||||
channelId,
|
||||
color: colors.palette.success200,
|
||||
color: colors.palette.success200,
|
||||
pressAction: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -44,14 +104,12 @@ const createLocalNotification = async function (title: string, body: string, lar
|
||||
}
|
||||
|
||||
|
||||
const updateLocalNotification = async function (id: string, update: { title: string, body: string}) {
|
||||
// Request permissions (required for iOS)
|
||||
// await notifee.requestPermission()
|
||||
/*const updateLocalNotification = async function (id: string, update: { title: string, body: string}) {
|
||||
|
||||
// Create a channel (required for Android)
|
||||
const channelId = await notifee.createChannel({
|
||||
id: 'default',
|
||||
name: 'Default Channel',
|
||||
name: 'Minibits notifications',
|
||||
})
|
||||
|
||||
const {title, body} = update
|
||||
@@ -64,11 +122,9 @@ const updateLocalNotification = async function (id: string, update: { title: str
|
||||
android: {
|
||||
channelId,
|
||||
color: colors.palette.success200,
|
||||
// smallIcon: 'name-of-a-small-icon', // optional, defaults to 'ic_launcher'.
|
||||
// pressAction is needed if you want the notification to open the app when pressed
|
||||
/* pressAction: {
|
||||
pressAction: {
|
||||
id: 'default',
|
||||
},*/
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -76,7 +132,7 @@ const updateLocalNotification = async function (id: string, update: { title: str
|
||||
|
||||
const cancelNotification = async function (id: string) {
|
||||
await notifee.cancelNotification(id)
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
const areNotificationsEnabled = async function (): Promise<boolean> {
|
||||
@@ -90,8 +146,9 @@ const areNotificationsEnabled = async function (): Promise<boolean> {
|
||||
}
|
||||
|
||||
export const NotificationService = {
|
||||
onReceiveRemoteNotification,
|
||||
createLocalNotification,
|
||||
updateLocalNotification,
|
||||
cancelNotification,
|
||||
// updateLocalNotification,
|
||||
// cancelNotification,
|
||||
areNotificationsEnabled
|
||||
}
|
||||
@@ -17,12 +17,12 @@ import {MeltQuoteResponse, type Proof as CashuProof} from '@cashu/cashu-ts'
|
||||
import {Mint} from '../models/Mint'
|
||||
import {pollerExists, stopPolling} from '../utils/poller'
|
||||
import EventEmitter from '../utils/eventEmitter'
|
||||
import { NostrClient, NostrEvent, NostrFilter, NostrProfile } from './nostrService'
|
||||
import { MINIBITS_NIP05_DOMAIN, MINIBITS_SERVER_API_HOST } from '@env'
|
||||
import { NostrClient, NostrEvent, NostrProfile } from './nostrService'
|
||||
import { MINIBITS_NIP05_DOMAIN, MINIBITS_SERVER_API_HOST, MINIBIT_SERVER_NOSTR_PUBKEY } from '@env'
|
||||
import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from '../models/PaymentRequest'
|
||||
import { IncomingDataType, IncomingParser } from './incomingParser'
|
||||
import { Contact } from '../models/Contact'
|
||||
import { getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils'
|
||||
import { getDecodedToken, getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils'
|
||||
import { SyncQueue } from './syncQueueService'
|
||||
import { receiveTask, receiveOfflinePrepareTask, receiveOfflineCompleteTask} from './wallet/receiveTask'
|
||||
import { sendTask } from './wallet/sendTask'
|
||||
@@ -31,6 +31,7 @@ import { transferTask } from './wallet/transferTask'
|
||||
import { WalletUtils } from './wallet/utils'
|
||||
import { NotificationService } from './notificationService'
|
||||
import { CurrencyCode, MintUnit, formatCurrency, getCurrency } from './wallet/currency'
|
||||
import { MinibitsClient } from './minibitsService'
|
||||
|
||||
|
||||
type WalletTaskService = {
|
||||
@@ -42,11 +43,12 @@ type WalletTaskService = {
|
||||
isPending: boolean
|
||||
}
|
||||
) => Promise<void>
|
||||
handleInFlight: () => Promise<void>
|
||||
handleInFlight: () => Promise<void>
|
||||
handlePendingTopups: () => Promise<void>
|
||||
handlePendingTopup: (params: {
|
||||
paymentRequest: PaymentRequest
|
||||
}) => Promise<void>
|
||||
handleClaim: () => Promise<void>
|
||||
receiveEventsFromRelays: () => Promise<void>
|
||||
transfer: (
|
||||
mintBalanceToTransferFrom: MintBalance,
|
||||
@@ -971,6 +973,70 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
|
||||
}
|
||||
|
||||
|
||||
const handleClaim = async function (): Promise<void> {
|
||||
|
||||
log.info('[handleClaim] start')
|
||||
const {walletId, seedHash, pubkey} = walletProfileStore
|
||||
|
||||
if(!walletId || !seedHash || !pubkey) {
|
||||
log.warn('[handleClaim] Missing or incomplete walletProfile, skipping claim request...')
|
||||
return
|
||||
}
|
||||
|
||||
const claimedInvoices = await MinibitsClient.createClaim(walletId, seedHash, pubkey)
|
||||
|
||||
log.debug(claimedInvoices)
|
||||
|
||||
if(claimedInvoices.length === 0) {
|
||||
log.info('[handleClaim] No claimed invoices returned from server...')
|
||||
return
|
||||
}
|
||||
|
||||
for(const claimedInvoice of claimedInvoices) {
|
||||
const now = new Date().getTime()
|
||||
|
||||
SyncQueue.addTask(
|
||||
`_handleClaimTask-${now}`,
|
||||
async () => await _handleClaimTask({encryptedToken: claimedInvoice.token})
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const _handleClaimTask = async function (params: {encryptedToken: string}) {
|
||||
const {encryptedToken} = params
|
||||
|
||||
if(!encryptedToken) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, '[_handleClaimTask] Missing encodedToken to receive.')
|
||||
}
|
||||
|
||||
const encodedToken = await NostrClient.decryptNip04(MINIBIT_SERVER_NOSTR_PUBKEY, encryptedToken)
|
||||
|
||||
log.debug('[_handleClaimTask] decrypted token', {encodedToken})
|
||||
|
||||
const decoded: Token = CashuUtils.decodeToken(encodedToken)
|
||||
const amountToReceive = CashuUtils.getTokenAmounts(decoded).totalAmount
|
||||
const memo = decoded.memo || 'Received to Lightning address'
|
||||
|
||||
const result: TransactionTaskResult = await receiveTask(
|
||||
decoded,
|
||||
amountToReceive,
|
||||
memo,
|
||||
encodedToken,
|
||||
)
|
||||
|
||||
return {
|
||||
mintUrl: decoded.token[0].mint,
|
||||
taskFunction: '_handleClaimTask',
|
||||
message: 'Ecash sent to your lightning address has been received.',
|
||||
proofsCount: decoded.token[0].proofs.length,
|
||||
proofsAmount: result.transaction?.amount,
|
||||
} as WalletTaskResult
|
||||
}
|
||||
|
||||
|
||||
|
||||
const _sendTopupNotification = async function (pr: PaymentRequest) {
|
||||
|
||||
@@ -1261,6 +1327,7 @@ const _sendReceiveNotification = async function (
|
||||
try {
|
||||
const zapSenderProfile: NostrProfile = JSON.parse(maybeZapSenderString)
|
||||
|
||||
|
||||
if(zapSenderProfile) {
|
||||
sentFromPubkey = zapSenderProfile.pubkey // zap sender pubkey
|
||||
sentFrom = zapSenderProfile.nip05 || zapSenderProfile.name
|
||||
@@ -1271,7 +1338,7 @@ const _sendReceiveNotification = async function (
|
||||
const contactInstance = contactsStore.findByPubkey(sentFromPubkey)
|
||||
if(contactInstance && sentFromLud16) {
|
||||
contactInstance.setLud16(sentFromLud16)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -1310,7 +1377,6 @@ const _extractZapSenderData = function (str: string) {
|
||||
|
||||
|
||||
|
||||
|
||||
export const WalletTask: WalletTaskService = {
|
||||
handleSpentFromPending,
|
||||
handleSpentFromSpendable,
|
||||
@@ -1318,6 +1384,7 @@ export const WalletTask: WalletTaskService = {
|
||||
handleInFlight,
|
||||
handlePendingTopups,
|
||||
handlePendingTopup,
|
||||
handleClaim,
|
||||
receiveEventsFromRelays,
|
||||
receive,
|
||||
receiveOfflinePrepare,
|
||||
|
||||
@@ -5,7 +5,8 @@ declare module '@env' {
|
||||
export const CODEPUSH_STAGING_DEPLOYMENT_KEY: string
|
||||
export const NATIVE_VERSION_ANDROID: string
|
||||
export const MINIBITS_SERVER_API_KEY: string
|
||||
export const MINIBITS_SERVER_API_HOST: string
|
||||
export const MINIBITS_SERVER_API_HOST: string
|
||||
export const MINIBIT_SERVER_NOSTR_PUBKEY: string
|
||||
export const MINIBITS_NIP05_DOMAIN: string
|
||||
export const MINIBITS_RELAY_URL: string
|
||||
export const MINIBITS_MINT_URL: string
|
||||
|
||||
@@ -1850,6 +1850,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.3.1.tgz#4539371a2f4bd402d932031be82c2449ed63a1a5"
|
||||
integrity sha512-UBnJxyV0b7i9Moa97Av+HKho1ByzX0DtbJXzUQS5E3xhQs6P2D/Os0iw3ouy7joY1TVd6uIhplPbr7l1SJNaNQ==
|
||||
|
||||
"@react-native-firebase/app@^20.0.0":
|
||||
version "20.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-firebase/app/-/app-20.0.0.tgz#0aae0fdaaa0db48f6b248fb410e18687e285f2e1"
|
||||
integrity sha512-h9Xgmj0Z0ga1ponhDnQ63yqkX288LOW+ZvstDWHDo2rgPu58nbAmdzs73MTmPjYdgm5Elm71puG1NRJX5n7wRw==
|
||||
dependencies:
|
||||
opencollective-postinstall "^2.0.3"
|
||||
superstruct "^0.6.2"
|
||||
|
||||
"@react-native-firebase/messaging@^20.0.0":
|
||||
version "20.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-20.0.0.tgz#92d25294361ef7e7341b916a903f8fc0abb53abc"
|
||||
integrity sha512-sBc3SRmUW9+T75YKa374kV4TfAz5EemBc9rXHYlgTI2DQeBhWNQbazAbKEJyyZOJDbs2RgR9f330VWCP7jLTBA==
|
||||
|
||||
"@react-native/assets-registry@0.73.1":
|
||||
version "0.73.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.73.1.tgz#e2a6b73b16c183a270f338dc69c36039b3946e85"
|
||||
@@ -3356,6 +3369,16 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone-deep@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
|
||||
integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==
|
||||
dependencies:
|
||||
for-own "^1.0.0"
|
||||
is-plain-object "^2.0.4"
|
||||
kind-of "^6.0.0"
|
||||
shallow-clone "^1.0.0"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||
@@ -4510,6 +4533,23 @@ for-each@^0.3.3:
|
||||
dependencies:
|
||||
is-callable "^1.1.3"
|
||||
|
||||
for-in@^0.1.3:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
|
||||
integrity sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==
|
||||
|
||||
for-in@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
|
||||
|
||||
for-own@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
|
||||
integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==
|
||||
dependencies:
|
||||
for-in "^1.0.1"
|
||||
|
||||
form-data@^2.3.3:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
@@ -5092,6 +5132,11 @@ is-docker@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||
|
||||
is-extendable@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
|
||||
integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
@@ -5866,7 +5911,12 @@ keyv@^4.5.3:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
kind-of@^6.0.2:
|
||||
kind-of@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
|
||||
integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
|
||||
|
||||
kind-of@^6.0.0, kind-of@^6.0.1, kind-of@^6.0.2:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
@@ -6385,6 +6435,14 @@ minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
mixin-object@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
|
||||
integrity sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==
|
||||
dependencies:
|
||||
for-in "^0.1.3"
|
||||
is-extendable "^0.1.1"
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
@@ -6659,6 +6717,11 @@ open@^7.0.3, open@^7.4.2:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
opencollective-postinstall@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
|
||||
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
|
||||
@@ -7786,6 +7849,15 @@ sha.js@^2.4.0, sha.js@^2.4.8:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
shallow-clone@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
|
||||
integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
|
||||
dependencies:
|
||||
is-extendable "^0.1.1"
|
||||
kind-of "^5.0.0"
|
||||
mixin-object "^2.0.1"
|
||||
|
||||
shallow-clone@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
|
||||
@@ -8129,6 +8201,14 @@ superagent@^8.0.0:
|
||||
qs "^6.11.0"
|
||||
semver "^7.3.8"
|
||||
|
||||
superstruct@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.2.tgz#c5eb034806a17ff98d036674169ef85e4c7f6a1c"
|
||||
integrity sha512-lvA97MFAJng3rfjcafT/zGTSWm6Tbpk++DP6It4Qg7oNaeM+2tdJMuVgGje21/bIpBEs6iQql1PJH6dKTjl4Ig==
|
||||
dependencies:
|
||||
clone-deep "^2.0.1"
|
||||
kind-of "^6.0.1"
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
|
||||
Reference in New Issue
Block a user