From b14f7057df4de1cb9954e41e72b175c8cebef387 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Wed, 29 May 2024 12:20:31 +0200 Subject: [PATCH] v0.1.8-beta - push notifications, claim ecash in batch, Wallet address recovery tool when moving to new device. --- .github/workflows/jsCodePush-PROD.yaml | 1 + .github/workflows/jsCodePush-TEST.yaml | 1 + android/app/build.gradle | 5 +- android/build.gradle | 1 + index.js | 7 + package.json | 4 +- src/App.tsx | 26 +- src/components/Loading.tsx | 2 +- src/i18n/en.ts | 6 +- src/models/WalletProfileStore.ts | 7 +- src/models/helpers/setupRootStore.ts | 49 +--- src/navigation/AppNavigator.tsx | 2 +- src/navigation/TabsNavigator.tsx | 3 + src/screens/BackupScreen.tsx | 22 +- src/screens/RemoteBackupScreen.tsx | 4 +- src/screens/RemoteRecoveryScreen.tsx | 314 +++++++++++++++++-------- src/screens/SettingsScreen.tsx | 126 +++++++--- src/screens/WalletScreen.tsx | 123 ++++++++-- src/screens/WelcomeScreen.tsx | 2 +- src/services/keyChain.ts | 2 +- src/services/logService.ts | 2 + src/services/minibitsService.ts | 50 +++- src/services/nostrService.ts | 3 +- src/services/notificationService.ts | 93 ++++++-- src/services/walletService.ts | 79 ++++++- src/utils/envtypes/env.d.tsx | 3 +- yarn.lock | 82 ++++++- 27 files changed, 779 insertions(+), 240 deletions(-) diff --git a/.github/workflows/jsCodePush-PROD.yaml b/.github/workflows/jsCodePush-PROD.yaml index f990fce4..7f93cbc6 100644 --- a/.github/workflows/jsCodePush-PROD.yaml +++ b/.github/workflows/jsCodePush-PROD.yaml @@ -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 diff --git a/.github/workflows/jsCodePush-TEST.yaml b/.github/workflows/jsCodePush-TEST.yaml index 7e0ae65d..d2b52bdf 100644 --- a/.github/workflows/jsCodePush-TEST.yaml +++ b/.github/workflows/jsCodePush-TEST.yaml @@ -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 diff --git a/android/app/build.gradle b/android/app/build.gradle index b20e92e1..c36aa142 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 { diff --git a/android/build.gradle b/android/build.gradle index af053a45..144db8a4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 } } diff --git a/index.js b/index.js index a701d0b1..8c9807a2 100644 --- a/index.js +++ b/index.js @@ -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 } + +// Setup notification listeners and handlers +messaging().onMessage(NotificationService.onReceiveRemoteNotification) +messaging().setBackgroundMessageHandler(NotificationService.onReceiveRemoteNotification) + AppRegistry.registerComponent(appName, () => BootstrapApp) diff --git a/package.json b/package.json index 01634aa3..4bcf09a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index a0d5eef0..d1a0bf14 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 } diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index 03c68aa4..b67e8f0d 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -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 ( 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( diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index f75ab90f..d67153ab 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -40,7 +40,7 @@ import { colors, useThemeColor } from "../theme" */ export type AppStackParamList = { Welcome: undefined - RemoteRecovery: undefined + RemoteRecovery: {isAddressOnlyRecovery: boolean} Mints: {} Tabs: NavigatorScreenParams } diff --git a/src/navigation/TabsNavigator.tsx b/src/navigation/TabsNavigator.tsx index 2c29cf49..9d43f67f 100644 --- a/src/navigation/TabsNavigator.tsx +++ b/src/navigation/TabsNavigator.tsx @@ -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() { + ) } diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx index 9e87d58d..c762ff69 100644 --- a/src/screens/BackupScreen.tsx +++ b/src/screens/BackupScreen.tsx @@ -121,7 +121,11 @@ export const BackupScreen: FC> = 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> = observer(fun style={$card} HeadingComponent={ <> - > = observer(fun leftIconInverse={true} style={$item} onPress={gotoRemoteBackup} - /> + bottomSeparator={true} + /> + } /> @@ -238,7 +252,7 @@ export const BackupScreen: FC> = observer(fun <> > = return ( - + > = ContentComponent={ > = o const indexInputRef = useRef(null) const [info, setInfo] = useState('') + const [isAddressOnlyRecovery, setIsAddressOnlyRecovery] = useState(route.params.isAddressOnlyRecovery ? true : false) const [mnemonic, setMnemonic] = useState('') const [mnemonicExists, setMnemonicExists] = useState(false) const [isValidMnemonic, setIsValidMnemonic] = useState(false) const [seed, setSeed] = useState() + const [profileToRecover, setProfileToRecover] = useState(undefined) const [selectedMintUrl, setSelectedMintUrl] = useState() const [selectedKeyset, setSelectedKeyset] = useState() const [selectedMintKeysets, setSelectedMintKeysets] = useState([]) @@ -166,7 +170,7 @@ export const RemoteRecoveryScreen: FC> = o const onAddMints = function (): void { - return navigation.navigate('Mints') + return navigation.navigate('Mints', {}) } @@ -473,6 +477,75 @@ export const RemoteRecoveryScreen: FC> = 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> = o - {mnemonicExists ? ( + {mnemonicExists && !isAddressOnlyRecovery ? ( > = o ) : ( <> {isValidMnemonic ? ( - <> + <> > = o /> } /> - - } - RightComponent={mintsStore.mintCount > 0 ? ( - + {isAddressOnlyRecovery ? ( + <> + {profileToRecover ? ( + <> + } + style={$item} + /> + } + /> +