Load proofs always from DB, theme app icon

This commit is contained in:
minibits-cash
2024-09-27 15:43:27 +02:00
parent e7eaddecd5
commit 3aa8bf5bfd
66 changed files with 344 additions and 158 deletions
+57 -42
View File
@@ -11,47 +11,62 @@
android:supportsRtl="true"
android:usesCleartextTraffic="true"
>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lightning" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lnurlw" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="lnurlp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="cashu" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="lightning"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="lnurlw"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="lnurlp"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="cashu"/>
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivityDefault"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityGolden"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_golden"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
</application>
</manifest>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_golden_background"/>
<foreground android:drawable="@mipmap/ic_launcher_golden_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_golden_monochrome"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -38,6 +38,7 @@
"@react-navigation/native-stack": "^6.11.0",
"@react-navigation/stack": "^6.4.1",
"@sentry/react-native": "^5.32.0",
"@synonymdev/react-native-change-icon": "^5.0.0",
"date-fns": "^4.0.0",
"i18n-js": "^4.4.3",
"js-lnurl": "^0.6.0",
+4 -4
View File
@@ -15,8 +15,8 @@
"missingMnemonicOrSeedError": "Missing mnemonic or seed.",
"mnemonicDesc": "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.",
"mnemonicTitle": "Your mnemonic phrase",
"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.",
"recoveryTool": "Export ecash",
"recoveryToolDescription": "Filter and export all ecash from local database grouped by mint keysets.",
"remoteBackupDescription": "Mnemonic phrase encodes your seed and allows you to recover your ecash balance in case of device loss.",
"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.",
@@ -231,7 +231,7 @@
"nsecMissingProfileError": "Missing profile to update",
"nsecPasteError": "Copy your nsec key first, then paste.",
"nsecPrivatePublicKeyMismatchError": "Provided private key does not match your new profile public key.",
"numberOfBackedupProofs": "Number of backed up proofs",
"numberOfBackedupProofs": "Number of stored proofs",
"onDeviceInfo": "On-device information",
"pasteEcashTokenDesc": "Paste ecash token you want to receive.",
"payCommon": {
@@ -370,7 +370,7 @@
"recoveryInsertMnemonicDesc": "Paste or rewrite 12 word phrase to recover your ecash balance and wallet address on this device. Separate words by blank spaces.",
"recoveryInsertMnemonicDescAddrOnly": "Paste or rewrite 12 word phrase to recover your wallet address on this device. Separate words by blank spaces.",
"recoveryInvalidMnemonicError": "Invalid mnemonic phrase. Provide 12 word sequence separated by blank spaces.",
"recoveryTool": "Recovery tool",
"exportEcash": "Export ecash",
"relayExists": "Relay already exists",
"relays": "Relays",
"relayurlPasteError": "Copy your relay URL key before pasting",
+46 -2
View File
@@ -4,9 +4,10 @@ import {
types,
isStateTreeNode,
detach,
flow,
} from 'mobx-state-tree'
import {withSetPropAction} from './helpers/withSetPropAction'
import {ProofModel, Proof} from './Proof'
import {ProofModel, Proof, BackupProof} from './Proof'
import {log} from '../services/logService'
import {getRootStore} from './helpers/getRootStore'
import AppError, {Err} from '../utils/AppError'
@@ -87,6 +88,43 @@ export const ProofsStoreModel = types
},
}))
.actions(self => ({
// Proofs are not more persisted in mmkv storage but loaded from database on afterCreate model
loadProofsFromDatabase: flow(function* laodProofs() {
const unspentAndPendingProofs = yield Database.getProofs(true, true, false)
// Group into isUnspent and isPending
const groupedProofs = unspentAndPendingProofs.reduce(
(acc: {isPending: BackupProof[], isUnspent: BackupProof[]}, proof: BackupProof) => {
if (proof.isPending) {
acc.isPending.push(proof);
} else {
acc.isUnspent.push(proof);
}
return acc;
},
{ isUnspent: [], isPending: [] } // Initialize the groups
)
const cleanProofs = (proofs: BackupProof[]) => {
return proofs.map(p => {
// Destructure the unwanted properties and collect the rest in `cleaned`
const { isPending, isSpent, updatedAt, ...cleaned } = p
// Return the cleaned object
return cleaned as Proof
})
}
self.proofs.replace(cleanProofs(groupedProofs.isUnspent))
self.pendingProofs.replace(cleanProofs(groupedProofs.isPending))
}),
}))
.actions(self => ({
/*afterCreate() {
// not used to be able to load proofs without blocking app start
// Trigger the load proofs action when the store is initialized
self.loadProofsFromDatabase()
},*/
addProofs(newProofs: Proof[], isPending: boolean = false): {addedAmount: number, addedProofs: Proof[]} {
try {
const proofs = isPending ? self.pendingProofs : self.proofs
@@ -405,7 +443,13 @@ export const ProofsStoreModel = types
getProofsSubset: (proofs: Proof[], proofsToRemove: Proof[]) => {
return proofs.filter(proof => !proofsToRemove.includes(proof))
},
}))
})).postProcessSnapshot((snapshot) => { // NOT persisted to storage except last pendingByMintSecrets!
return {
proofs: [],
pendingProofs: [],
pendingByMintSecrets: snapshot.pendingByMintSecrets
}
})
export interface Proofs extends Instance<typeof ProofsStoreModel> {}
+1 -1
View File
@@ -11,7 +11,7 @@ import {WalletStoreModel} from './WalletStore'
import {NwcStoreModel} from './NwcStore'
import { log } from '../services'
export const rootStoreModelVersion = 26 // Update this if model changes require migrations defined in setupRootStore.ts
export const rootStoreModelVersion = 28 // Update this if model changes require migrations defined in setupRootStore.ts
/**
* A RootStore model.
+2 -2
View File
@@ -103,7 +103,7 @@ export const UserSettingsStoreModel = types
self.isLocalBackupOn = isLocalBackupOn
return isLocalBackupOn
},
setIsStorageEncrypted: flow(function* setIsStorageEncryptedvalue(
setIsStorageEncrypted: flow(function* setIsStorageEncryptedvalue( // legacy, tobe removed
isEncrypted: boolean,
) {
if (isEncrypted) {
@@ -115,7 +115,7 @@ export const UserSettingsStoreModel = types
self.isStorageEncrypted = isEncrypted
return isEncrypted
}),
setIsTorDaemonOn: (isTorDaemonOn: boolean) => {
setIsTorDaemonOn: (isTorDaemonOn: boolean) => { // legacy, tobe removed
Database.updateUserSettings({...self, isTorDaemonOn})
self.isTorDaemonOn = isTorDaemonOn
return isTorDaemonOn
+39 -9
View File
@@ -54,6 +54,7 @@ export async function setupRootStore(rootStore: RootStore) {
Sentry.setUser({ id: userSettings.walletId })
}
// legacy, encryption is sunset, to be replaced by opt-in biometric auth to read the seed
if (userSettings.isStorageEncrypted) {
try {
await MMKVStorage.initEncryption()
@@ -77,21 +78,27 @@ export async function setupRootStore(rootStore: RootStore) {
}
}
}
}
} // legacy end
// load the last known state from storage
const start = performance.now()
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
// dirty mobx fix
restoredState.walletProfileStore.isBatchClaimOn = undefined
const mmkvLoaded = performance.now()
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
log.trace('[setupRootStore]', `Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`)
log.trace('[setupRootStore]', `restored state has ${dataSize} bytes.`)
// log.trace(restoredState)
applySnapshot(rootStore, restoredState)
const stateHydrated = performance.now()
log.trace(`[setupRootStore] Hydrating rooStoreModel took ${stateHydrated - mmkvLoaded} ms.`)
const {proofsStore} = rootStore
await proofsStore.loadProofsFromDatabase()
const proofsLoaded = performance.now()
log.trace(`[setupRootStore] Loading proofs from DB and hydrating took ${proofsLoaded - stateHydrated} ms.`)
} catch (e: any) {
log.error('[setupRootStore]', Err.STORAGE_ERROR, {message: e.message, params: e.params})
}
@@ -326,6 +333,30 @@ async function _runMigrations(rootStore: RootStore) {
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
if(currentVersion < 28) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v27`)
if(userSettingsStore.isStorageEncrypted) {
await userSettingsStore.setIsStorageEncrypted(false)
}
if(!userSettingsStore.isLocalBackupOn) {
const proofs = proofsStore.allProofs
const pendingProofs = proofsStore.pendingProofs
Database.addOrUpdateProofs(proofs, false, false)
Database.addOrUpdateProofs(pendingProofs, true, false)
userSettingsStore.setIsLocalBackupOn(true)
}
for (const mint of mintsStore.allMints) {
for(const keysetId of mint.keysetIds) {
Database.updateProofsMintUrl(keysetId, mint.mintUrl)
}
}
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
} catch (e: any) {
throw new AppError(
Err.STORAGE_ERROR,
@@ -334,5 +365,4 @@ async function _runMigrations(rootStore: RootStore) {
)
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ import {
UpdateScreen,
BackupScreen,
RemoteBackupScreen,
LocalRecoveryScreen,
ExportEcashScreen,
RelaysScreen,
TranDetailScreen,
TranHistoryScreen,
@@ -287,7 +287,7 @@ export type SettingsStackParamList = {
}
Backup: undefined
RemoteBackup: undefined
LocalRecovery: undefined
ExportEcash: undefined
Developer: undefined
Relays: undefined
RemoteRecovery: {isAddressOnlyRecovery: boolean}
@@ -320,7 +320,7 @@ const SettingsNavigator = function SettingsNavigator() {
<SettingsStack.Screen name="Update" component={UpdateScreen} />
<SettingsStack.Screen name="Backup" component={BackupScreen} />
<SettingsStack.Screen name="RemoteBackup" component={RemoteBackupScreen} />
<SettingsStack.Screen name="LocalRecovery" component={LocalRecoveryScreen} />
<SettingsStack.Screen name="ExportEcash" component={ExportEcashScreen} />
<SettingsStack.Screen name="Developer" component={DeveloperScreen} />
<SettingsStack.Screen name="Relays" component={RelaysScreen} />
<SettingsStack.Screen name="RemoteRecovery" component={RemoteRecoveryScreen} />
+23 -13
View File
@@ -20,7 +20,7 @@ import {useStores} from '../models'
import AppError from '../utils/AppError'
import EventEmitter from '../utils/eventEmitter'
import {ResultModalInfo} from './Wallet/ResultModalInfo'
import {Database, log, WalletTask, WalletTaskResult} from '../services'
import {Database, log, SyncStateTaskResult, WalletTask, WalletTaskResult} from '../services'
import { translate } from '../i18n'
export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(function BackupScreen(_props) {
@@ -41,22 +41,32 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
const [totalSpentAmount, setTotalSpentAmount] = useState<number>(0)
useEffect(() => {
const removeSpentByMintTaskResult = async (result: WalletTaskResult) => {
const removeSpentByMintTaskResult = async (result: SyncStateTaskResult) => {
log.trace('removeSpentByMintTaskResult event handler triggered')
if (!isSyncStateSentToQueue) { return false }
setIsLoading(false)
// runs per each mint
if (result && result.spentAmount > 0) {
if (result && result.transactionStateUpdates.length > 0) {
let totalSpentPerMint = 0
for (const update of result.transactionStateUpdates) {
if(update.spentByMintAmount && update.spentByMintAmount > 0) {
totalSpentPerMint += update.spentByMintAmount
}
}
setTotalSpentAmount(prev => prev + result.spentAmount)
setTotalSpentCount(prev => prev + result.spentCount)
setInfo(
translate("backupScreen.result", { proofCount: totalSpentCount, amount: totalSpentAmount })
translate("backupScreen.result", { proofCount: totalSpentCount, amount: totalSpentAmount })
)
return
}
setInfo(translate("noSpentEcashFound"))
setInfo(translate("noSpentEcashFound"))
}
EventEmitter.on('ev__syncStateWithMintTask_result', removeSpentByMintTaskResult)
@@ -66,7 +76,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
}
}, [isSyncStateSentToQueue])
const toggleBackupSwitch = () => {
/* const toggleBackupSwitch = () => {
try {
setIsLoading(true)
const result = userSettingsStore.setIsLocalBackupOn(!isLocalBackupOn)
@@ -97,13 +107,13 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
} catch (e: any) {
handleError(e)
}
}
}*/
const toggleBackupModal = () =>
setIsBackupModalVisible(previousState => !previousState)
const gotoLocalRecovery = function () {
navigation.navigate('LocalRecovery')
const gotoExportEcash = function () {
navigation.navigate('ExportEcash')
}
const gotoRemoteBackup = function () {
@@ -180,7 +190,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
style={$card}
HeadingComponent={
<>
<ListItem
{/*<ListItem
tx="backupScreen.localBackup"
subTx="backupScreen.localBackupDescription"
leftIcon='faDownload'
@@ -199,7 +209,7 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
</View>
}
style={$item}
/>
/>*/}
{isLocalBackupOn && (
<ListItem
tx="backupScreen.recoveryTool"
@@ -208,8 +218,8 @@ export const BackupScreen: FC<SettingsStackScreenProps<'Backup'>> = observer(fun
leftIconColor={colors.palette.focus300}
leftIconInverse={true}
style={$item}
onPress={gotoLocalRecovery}
topSeparator={true}
onPress={gotoExportEcash}
// topSeparator={true}
/>
)}
</>
@@ -4,10 +4,8 @@ import {
TextStyle,
ViewStyle,
View,
useColorScheme,
Alert,
} from 'react-native'
import {useThemeColor, spacing, colors, typography} from '../theme'
import {useThemeColor, spacing, typography} from '../theme'
import {
Button,
Icon,
@@ -28,28 +26,24 @@ import {BackupProof} from '../models/Proof'
import { useStores } from '../models'
import { CashuUtils, ProofV3, TokenV3 } from '../services/cashu/cashuUtils'
import Clipboard from '@react-native-clipboard/clipboard'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction'
import { WalletUtils } from '../services/wallet/utils'
import { MintUnit, MintUnits, getCurrency } from '../services/wallet/currency'
import { MintUnits, getCurrency } from '../services/wallet/currency'
import { CurrencyAmount } from './Wallet/CurrencyAmount'
import { translate } from '../i18n'
interface LocalRecoveryScreenProps
extends SettingsStackScreenProps<'LocalRecovery'> {}
interface ExportEcashScreenProps extends SettingsStackScreenProps<'ExportEcash'> {}
export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
function LocalRecoveryScreen(_props) {
export const ExportEcashScreen: FC<ExportEcashScreenProps> =
function ExportEcash(_props) {
const { navigation } = _props
const { mintsStore, proofsStore, transactionsStore } = useStores()
const { mintsStore } = useStores()
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const [showUnspentOnly, setShowUnspentOnly] = useState<boolean>(true)
const [showPendingOnly, setShowPendingOnly] = useState<boolean>(false)
const [showSpentOnly, setShowSpentOnly] = useState<boolean>(false)
@@ -72,7 +66,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
try {
setIsLoading(true)
// update empty unit fields to resolve v0.1.7 upgrade issue of backed up proofs not having unit migrated
await Database.updateProofsToDefaultUnit()
// await Database.updateProofsToDefaultUnit()
const backupProofs = await Database.getProofs(isUnspent, isPending, isDeleted)
setProofs(backupProofs)
setIsLoading(false)
@@ -188,7 +182,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
}
const onRecovery = async function () {
/* const onRecovery = async function () {
if (!showUnspentOnly) {
setInfo(translate("unspentOnlyRecoverable"))
return
@@ -337,7 +331,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
} catch (e: any) {
handleError(e)
}
}
} */
const groupProofsByMint = function (proofs: BackupProof[]) {
return proofs.reduce((acc: Record<string, BackupProof[]>, proof) => {
@@ -383,7 +377,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
const headerBg = useThemeColor('header')
const iconColor = useThemeColor('textDim')
const activeIconColor = useThemeColor('button')
const activeIconColor = useThemeColor('tint')
const headerTitle = useThemeColor('headerTitle')
@@ -392,7 +386,7 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text
preset="heading"
tx="recoveryTool"
tx="exportEcash"
style={{color: headerTitle}}
/>
</View>
@@ -474,44 +468,36 @@ export const LocalRecoveryScreen: FC<LocalRecoveryScreenProps> =
))}
</>
}
FooterComponent={
<View style={$buttonContainer}>
<Button
preset="tertiary"
onPress={copyBackupProofs}
tx="copyProofs"
style={{
minHeight: 25,
paddingVertical: spacing.extraSmall,
marginTop: spacing.small,
marginRight: spacing.small
}}
textStyle={{fontSize: 14}}
/>
<Button
preset="tertiary"
onPress={copyEncodedTokens}
tx="copyAsEncodedTokens"
style={{
minHeight: 25,
paddingVertical: spacing.extraSmall,
marginTop: spacing.small,
}}
textStyle={{fontSize: 14}}
/>
</View>
}
style={[$card]}
/>
)}
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
// preset="secondary"
onPress={copyBackupProofs}
tx="copyProofs"
style={{
marginRight: spacing.small
}}
textStyle={{fontSize: 14}}
/>
<Button
// preset="secondary"
onPress={copyEncodedTokens}
tx="copyAsEncodedTokens"
textStyle={{fontSize: 14}}
/>
</View>
</View>
{/* <View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
onPress={onRecovery}
tx="recoverToWallet"
/>
</View>
</View>
</View> */}
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
+2 -2
View File
@@ -16,9 +16,9 @@ import { QRCodeBlock } from './Wallet/QRCode'
import { CollapsibleText } from '../components/CollapsibleText'
import { isSameDay } from 'date-fns/isSameDay'
interface SettingsScreenProps extends SettingsStackScreenProps<'Nwc'> {}
interface NwcScreenProps extends SettingsStackScreenProps<'Nwc'> {}
export const NwcScreen: FC<SettingsScreenProps> = observer(
export const NwcScreen: FC<NwcScreenProps> = observer(
function NwcScreen(_props) {
const {navigation} = _props
+2 -2
View File
@@ -109,7 +109,7 @@ export const SecurityScreen: FC<SettingsStackScreenProps<'Security'>> = observer
style={$card}
ContentComponent={
<>
<ListItem
{/*<ListItem
tx="securityScreen.encryptStorage"
subTx="securityScreen.encryptStorageDescription"
leftIcon={isStorageEncrypted ? 'faLock' : 'faLockOpen'}
@@ -128,7 +128,7 @@ export const SecurityScreen: FC<SettingsStackScreenProps<'Security'>> = observer
</View>
}
style={$item}
/>
/>*/}
{isStorageEncrypted && (
<ListItem
tx="securityScreen.biometry"
+48 -15
View File
@@ -1,6 +1,6 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useCallback, useEffect, useRef, useState} from 'react'
import {Alert, AppState, TextStyle, View, ViewStyle} from 'react-native'
import {Alert, AppState, TextStyle, View, ViewStyle, useColorScheme} from 'react-native'
import notifee from '@notifee/react-native'
import messaging from '@react-native-firebase/messaging'
import {
@@ -9,9 +9,10 @@ import {
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
} from '@env'
import codePush, { RemotePackage } from 'react-native-code-push'
import { changeIcon, getIcon, resetIcon } from '@synonymdev/react-native-change-icon'
import {ThemeCode, Themes, colors, spacing, useThemeColor} from '../theme'
import {SettingsStackScreenProps} from '../navigation' // @demo remove-current-line
import {ListItem, Screen, Text, Card, NwcIcon, Button, BottomModal, InfoModal} from '../components'
import {ListItem, Screen, Text, Card, NwcIcon, Button, BottomModal, InfoModal, Icon} from '../components'
import {useHeader} from '../utils/useHeader'
import {useStores} from '../models'
import {translate} from '../i18n'
@@ -44,7 +45,8 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
} = useStores()
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
const [updateDescription, setUpdateDescription] = useState<string>('')
const [updateDescription, setUpdateDescription] = useState<string>('')
const [currentTheme, setCurrentTheme] = useState<ThemeCode>(userSettingsStore.theme)
const [updateSize, setUpdateSize] = useState<string>('')
const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState<boolean>(false)
@@ -180,9 +182,9 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
await notifee.openNotificationSettings()
}
const gotoPreferredUnit = function() {
/* const gotoPreferredUnit = function() {
Alert.alert('Preferred unit is set based on your Wallet screen.')
}
} */
const getRateColor = function () {
@@ -221,19 +223,42 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
toggleCurrencyModal()
}
const onSelectTheme = function(theme: ThemeCode) {
const currentTheme = userSettingsStore.theme
const onSelectTheme = async function(theme: ThemeCode) {
if(currentTheme !== theme) {
try {
// state update causes crash because of hooks
Database.updateUserSettings({...userSettingsStore, theme})
setCurrentTheme(theme)
const currentIcon = await getIcon()
log.trace('[onSelectTheme]', {currentIcon})
if(theme === ThemeCode.GOLDEN) {
await changeIcon('Golden')
const updatedIcon = await getIcon()
log.trace('[onSelectTheme]', {updatedIcon})
}
if(currentIcon !== 'Default') {
await changeIcon('Default')
const updatedIcon = await getIcon()
log.trace('[onSelectTheme]', {updatedIcon})
}
setInfo('Restart the wallet to apply new theme.')
} catch (e: any) {
log.warn('[onSelectTheme]', e.message)
}
}
toggleThemeModal()
}
const $itemRight = {color: useThemeColor('textDim')}
const headerBg = useThemeColor('header')
const headerTitle = useThemeColor('headerTitle')
const headerTitle = useThemeColor('headerTitle')
const colorScheme = useColorScheme()
const defaultThemeColor = colorScheme === 'dark' ? Themes[ThemeCode.DARK]?.color : Themes[ThemeCode.LIGHT]?.color
return (
<Screen contentContainerStyle={$screen} preset='auto'>
@@ -279,19 +304,20 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
style={$itemRight}
/>
</View>
}
}
bottomSeparator={true}
onPress={toggleCurrencyModal}
/>
<ListItem
tx='settingsScreen.theme'
leftIcon='faPaintbrush'
leftIconColor={headerBg as string}
leftIconColor={currentTheme === ThemeCode.DEFAULT ? defaultThemeColor as string : Themes[currentTheme as ThemeCode]?.color as string}
leftIconInverse={true}
style={$item}
RightComponent={
<View style={$rightContainer}>
<Text
text={Themes[userSettingsStore.theme]!.title}
text={Themes[currentTheme]!.title}
style={$itemRight}
/>
</View>
@@ -462,19 +488,26 @@ export const SettingsScreen: FC<SettingsScreenProps> = observer(
/>
<BottomModal
isVisible={isThemeModalVisible ? true : false}
style={{alignItems: 'stretch'}}
style={{alignItems: 'stretch', padding: spacing.small}}
ContentComponent={
<>
{[ThemeCode.DEFAULT, ThemeCode.DARK, ThemeCode.LIGHT, ThemeCode.GOLDEN].map(code =>
{[ThemeCode.DEFAULT, ThemeCode.DARK, ThemeCode.LIGHT, ThemeCode.GOLDEN].map(code =>
<ListItem
key={code}
leftIconColor={Themes[code as ThemeCode]?.color as string}
leftIconColor={code === ThemeCode.DEFAULT ? defaultThemeColor as string : Themes[code as ThemeCode]?.color as string}
leftIconInverse={true}
leftIcon='faPaintbrush'
text={Themes[code as ThemeCode]!.title}
onPress={() => onSelectTheme(code as ThemeCode)}
RightComponent={
<View style={$rightContainer}>
{currentTheme === code && (
<Icon icon='faCheckCircle' color={$itemRight.color} />
)}
</View>
}
bottomSeparator={true}
/>
/>
)}
</>
}
+7 -4
View File
@@ -39,7 +39,7 @@ import {useStores} from '../models'
import {WalletStackScreenProps} from '../navigation'
import {Mint, UnitBalance} from '../models/Mint'
import {MintsByUnit} from '../models/MintsStore'
import {log, MinibitsClient, NostrClient} from '../services'
import {Database, log, MinibitsClient, NostrClient} from '../services'
import {Env} from '../utils/envtypes'
import {Transaction} from '../models/Transaction'
import {TransactionListItem} from './Transactions/TransactionListItem'
@@ -63,6 +63,8 @@ import { LeftProfileHeader } from './ContactsScreen'
import { maxTransactionsByUnit } from '../models/TransactionsStore'
import { NavigationState, Route, TabBar, TabView } from 'react-native-tab-view'
import { NwcConnection, NwcConnectionModel } from '../models/NwcStore'
import { getSnapshot } from 'mobx-state-tree'
import { BackupProof, Proof } from '../models/Proof'
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
@@ -97,7 +99,6 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
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)
@@ -149,7 +150,8 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
// On app start
useEffect(() => {
// get deeplink if any
const getInitialData = async () => {
const getInitialData = async () => {
// get deeplink data if any
const url = await Linking.getInitialURL()
if (url) {
@@ -525,6 +527,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
renderLabel={({ route, focused, color }) => (
<CurrencySign
mintUnit={route.key as MintUnit}
textStyle={{color: 'white'}}
containerStyle={focused ? {} : {opacity: 0.5}}
/>
)}
@@ -796,7 +799,7 @@ const UnitBalanceBlock = observer(function (props: {
<CurrencyAmount
amount={getConvertedBalance() ?? 0}
currencyCode={unitBalance.unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
symbolStyle={{color: convertedBalanceColor, marginTop: spacing.tiny}}
symbolStyle={{color: convertedBalanceColor, marginTop: spacing.tiny, fontSize: moderateVerticalScale(10)}}
amountStyle={{color: convertedBalanceColor}}
size='small'
/>
+1 -1
View File
@@ -33,7 +33,7 @@ export * from './UpdateScreen'
export * from './BackupScreen'
export * from './RemoteBackupScreen'
export * from './RemoteRecoveryScreen'
export * from './LocalRecoveryScreen'
export * from './ExportEcashScreen'
export * from './DeveloperScreen'
export * from './RelaysScreen'
export * from './NwcScreen'
+3 -2
View File
@@ -10,6 +10,7 @@ import { NostrClient, NostrEvent, NostrProfile } from './nostrService'
import AppError, { Err } from '../utils/AppError'
import { Platform } from 'react-native'
import { RootStoreModel, rootStoreInstance, setupRootStore } from '../models';
import { ProofsStoreModel } from '../models/ProofsStore';
export type NotifyReceiveToLnurlData = {
type: 'NotifyReceiveToLnurlData',
@@ -90,11 +91,11 @@ const _receiveToLnurlHandler = async function(remoteData: NotifyReceiveToLnurlDa
const _nwcRequestHandler = async function(remoteData: NotifyNwcRequestData) {
const {requestEvent} = remoteData.data
const {nwcStore} = rootStoreInstance
const {nwcStore, proofsStore} = rootStoreInstance
// If app is killed, state is not loaded
if(nwcStore.all.length === 0) {
await setupRootStore(rootStoreInstance)
await setupRootStore(rootStoreInstance)
}
log.trace('[_nwcRequestHandler]', {connections: nwcStore.all})
+57 -9
View File
@@ -21,7 +21,7 @@ import { ThemeCode } from '../theme'
let _db: QuickSQLiteConnection
const _dbVersion = 15 // Update this if db changes require migrations
const _dbVersion = 16 // Update this if db changes require migrations
const getInstance = function () {
if (!_db) {
@@ -99,6 +99,7 @@ const _createOrUpdateSchema = function (db: QuickSQLiteConnection) {
C TEXT NOT NULL,
unit TEXT,
tId INTEGER,
mintUrl TEXT,
isPending BOOLEAN,
isSpent BOOLEAN,
updatedAt TEXT
@@ -294,6 +295,15 @@ const _runMigrations = function (db: QuickSQLiteConnection) {
log.info(`Prepared database migrations from ${currentVersion} -> 15`)
}
if (currentVersion < 16) {
migrationQueries.push([
`ALTER TABLE proofs
ADD COLUMN mintUrl`,
])
log.info(`Prepared database migrations from ${currentVersion} -> 16`)
}
// Update db version as a part of migration sqls
migrationQueries.push([
`INSERT OR REPLACE INTO dbversion (id, version, createdAt)
@@ -903,7 +913,7 @@ const updateOutputToken = function (id: number, outputToken: string) {
}
}
// proof of payment, e.g. preimage
const updateProof = function (id: number, proof: string) {
try {
const query = `
@@ -1073,8 +1083,8 @@ const addOrUpdateProof = function (
const now = new Date()
const query = `
INSERT OR REPLACE INTO proofs (id, amount, secret, C, tId, isPending, isSpent, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT OR REPLACE INTO proofs (id, amount, secret, C, tId, mintUrl, isPending, isSpent, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`
const params = [
proof.id,
@@ -1082,6 +1092,7 @@ const addOrUpdateProof = function (
proof.secret,
proof.C,
proof.tId,
proof.mintUrl,
isPending,
isSpent,
now.toISOString(),
@@ -1090,7 +1101,7 @@ const addOrUpdateProof = function (
const db = getInstance()
const result = db.execute(query, params)
// DO NOT log proof secrets to Sentry
log.info('[addOrUpdateProof]', `${isPending ? ' Pending' : ''} proof added or updated in the database backup`,
log.info('[addOrUpdateProof]', `${isPending ? ' Pending' : ''} proof added or updated in the database`,
{id: result.insertId, tId: proof.tId, isPending, isSpent},
)
@@ -1118,8 +1129,8 @@ const addOrUpdateProofs = function (
for (const proof of proofs) {
insertQueries.push([
` INSERT OR REPLACE INTO proofs (id, amount, secret, C, unit, tId, isPending, isSpent, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
` INSERT OR REPLACE INTO proofs (id, amount, secret, C, unit, tId, mintUrl, isPending, isSpent, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
[
proof.id,
@@ -1128,6 +1139,7 @@ const addOrUpdateProofs = function (
proof.C,
proof.unit,
proof.tId,
proof.mintUrl,
isPending,
isSpent,
now.toISOString(),
@@ -1145,7 +1157,7 @@ const addOrUpdateProofs = function (
// DO NOT log proof secrets to Sentry
log.info('[addOrUpdateProofs]',
`${rowsAffected}${isPending ? ' pending' : ''
} proofs were added or updated in the database backup`,
} proofs were added or updated in the database`,
{isPending, isSpent}
)
@@ -1159,6 +1171,31 @@ const addOrUpdateProofs = function (
}
}
// migration
const updateProofsMintUrl = function (id: string, mintUrl: string) {
try {
const query = `
UPDATE proofs
SET mintUrl = ?
WHERE id = ?
`
const params = [mintUrl, id]
const db = getInstance()
db.execute(query, params)
log.debug('[updateMintUrl]', 'Proof mintUrl updated', {id, mintUrl})
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
'Could not update proof mintUrl in database',
e.message,
)
}
}
const removeAllProofs = async function () {
try {
const query = `
@@ -1229,10 +1266,20 @@ const getProofs = async function (
ORDER BY id DESC
`
}
if (isUnspent && isPending) {
if (isPending) {
query = `
SELECT *
FROM proofs
WHERE isSpent = 0
ORDER BY id DESC
`
}
}
const db = getInstance()
const {rows} = await db.executeAsync(query)
return rows?._array as BackupProof[]
} catch (e: any) {
@@ -1315,6 +1362,7 @@ export const Database = {
getPendingAmount,
addOrUpdateProof,
addOrUpdateProofs,
updateProofsMintUrl,
removeAllProofs,
getProofById,
getProofs,
-1
View File
@@ -14,7 +14,6 @@ import {MintBalance, MintProofsCounter, MintStatus} from '../models/Mint'
import {MeltQuoteResponse, MintQuoteState} from '@cashu/cashu-ts'
import {Mint} from '../models/Mint'
import {pollerExists, stopPolling} from '../utils/poller'
import EventEmitter from '../utils/eventEmitter'
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'
+1 -1
View File
@@ -23,7 +23,7 @@ const palette = {
neutral500: '#978F8A',
neutral600: '#564E4A',
neutral700: '#2e2e2e',
neutral800: '#232323',
neutral800: '#181818',
neutral900: '#050505',
primary100: '#D6E4FF',
+10
View File
@@ -3867,6 +3867,15 @@ __metadata:
languageName: node
linkType: hard
"@synonymdev/react-native-change-icon@npm:^5.0.0":
version: 5.0.0
resolution: "@synonymdev/react-native-change-icon@npm:5.0.0"
peerDependencies:
react-native: "*"
checksum: 01e049715dc01ffdb081badd1803fbbb3630037be526224ff0ccec8d7aea8495b49bbafc0ace0926c8fbc1ac416ec5976626527b786586334fa3a964805db2c8
languageName: node
linkType: hard
"@tootallnate/quickjs-emscripten@npm:^0.23.0":
version: 0.23.0
resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0"
@@ -9381,6 +9390,7 @@ __metadata:
"@react-navigation/native-stack": ^6.11.0
"@react-navigation/stack": ^6.4.1
"@sentry/react-native": ^5.32.0
"@synonymdev/react-native-change-icon": ^5.0.0
"@tsconfig/react-native": ^3.0.0
"@types/lodash.clonedeep": ^4.5.9
"@types/react": ^18.2.6