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}
+ />
+ }
+ />
+
+ onPress={onCompleteAddress}
+ text={'Complete recovery'}
+ style={{marginRight: spacing.small}}
+ />
- ) : (undefined)
- }
- style={$item}
- />
- {mintsStore.mintCount === 0 && (
+ >
+ ) : (
+ onPress={onFindWalletAddress}
+ text={'Find wallet address'}
+ style={{marginRight: spacing.small}}
+ />
)}
- >
- }
- ContentComponent={
- <>
- {mintsStore.mints.map((mint: Mint, index: number) => (
- onMintSelect(mint)}
- isSelectable={true}
- isSelected={selectedMintUrl === mint.mintUrl}
- separator={index === 0 ? 'both' : 'bottom'}
- />
- ))}
- >
- }
- FooterComponent={
- <>
- {mintsStore.mintCount > 0 && selectedMintUrl && (
- <>
-
- {(startIndex > 0 || totalRecoveredAmount > 0) && (
+ >
+ ) : (
+ <>
+
+ }
+ RightComponent={mintsStore.mintCount > 0 ? (
+
+
+
+ ) : (undefined)
+ }
+ style={$item}
+ />
+ {mintsStore.mintCount === 0 && (
+
- )}
-
+ onPress={onAddMints}
+ text='Add mints'
+ />
+
+ )}
+ >
+ }
+ ContentComponent={
+ <>
+ {mintsStore.mints.map((mint: Mint, index: number) => (
+ onMintSelect(mint)}
+ isSelectable={true}
+ isSelected={selectedMintUrl === mint.mintUrl}
+ separator={index === 0 ? 'both' : 'bottom'}
+ />
+ ))}
+ >
+ }
+ FooterComponent={
+ <>
+ {mintsStore.mintCount > 0 && selectedMintUrl && (
+ <>
+
+ {(startIndex > 0 || totalRecoveredAmount > 0) && (
+
+ )}
+
-
-
-
-
-
-
-
-
- {selectedMintKeysets.length > 1 && (
-
+
+
+
+
+
+
+
-
- )}
-
- >
- )}
- {mintsStore.mintCount > 0 && !selectedMintUrl && (
-
- )}
- >
- }
- />
- >
+ />
+ {selectedMintKeysets.length > 1 && (
+
+
+
+ )}
+
+ >
+ )}
+ {mintsStore.mintCount > 0 && !selectedMintUrl && (
+
+ )}
+ >
+ }
+ />
+ >
+ )}
+ >
) : (
}
style={$item}
/>
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index decdd162..f99560ec 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -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 = 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(false)
const [updateDescription, setUpdateDescription] = useState('')
const [updateSize, setUpdateSize] = useState('')
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState(false)
+ const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(false)
useEffect(() => {
const checkForUpdate = async () => {
@@ -54,6 +58,43 @@ export const SettingsScreen: FC = 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 = 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 = observer(
text={getCurrency(userSettingsStore.preferredUnit).code}
/>
- }
- bottomSeparator={true}
+ }
+ bottomSeparator={false}
onPress={gotoPreferredUnit}
/>
+ >
+ }
+ />
+
+
+
+
+ }
+ bottomSeparator={true}
+ onPress={openNotificationSettings}
+ />
+
+
+
+ }
+ style={$item}
+ onPress={gotoRelays}
+ />
+ >
+ }
+ />
+
= observer(
>
}
/>
-
-
-
- }
- style={$item}
- onPress={gotoRelays}
- />
- }
- />
+
)
@@ -231,7 +303,7 @@ export const SettingsScreen: FC = observer(
)
const $screen: ViewStyle = {
- flex: 1,
+ // flex: 1,
}
const $headerContainer: TextStyle = {
diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx
index b437258f..aca9a9a4 100644
--- a/src/screens/WalletScreen.tsx
+++ b/src/screens/WalletScreen.tsx
@@ -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 = observer(
const [currentUnit, setCurrentUnit] = useState(groupedMints.length > 0 ? groupedMints[0].unit : 'sat')
const [info, setInfo] = useState('')
const [defaultMintUrl, setDefaultMintUrl] = useState(MINIBITS_MINT_URL)
+ const [lastClaimCheck, setLastClaimCheck] = useState(getUnixTime(new Date()))
const [error, setError] = useState()
const [isLoading, setIsLoading] = useState(false)
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false)
+ const [isSendModalVisible, setIsSendModalVisible] = useState(false)
+ const [isReceiveModalVisible, setIsReceiveModalVisible] = useState(false)
const [updateDescription, setUpdateDescription] = useState('')
const [updateSize, setUpdateSize] = useState('')
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState(false)
+ // On app start
useEffect(() => {
const checkForUpdate = async () => {
try {
@@ -136,21 +141,22 @@ export const WalletScreen: FC = 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 = observer(
}, initial: false})
}
-
+
useFocusEffect(
useCallback(() => {
if(!isInternetReachable) {
@@ -205,7 +211,18 @@ export const WalletScreen: FC = 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 = 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 = 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 = 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 = 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 = 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 = observer(
}
onBackButtonPress={toggleUpdateModal}
onBackdropPress={toggleUpdateModal}
+ />
+
+
+
+ >
+ }
+ onBackButtonPress={toggleSendModal}
+ onBackdropPress={toggleSendModal}
+ />
+
+
+
+ >
+ }
+ onBackButtonPress={toggleReceiveModal}
+ onBackdropPress={toggleReceiveModal}
/>
diff --git a/src/screens/WelcomeScreen.tsx b/src/screens/WelcomeScreen.tsx
index 64771cba..eadca58c 100644
--- a/src/screens/WelcomeScreen.tsx
+++ b/src/screens/WelcomeScreen.tsx
@@ -104,7 +104,7 @@ export const WelcomeScreen: FC> =
const gotoRecovery = async function () {
try {
- navigation.navigate('RemoteRecovery')
+ navigation.navigate('RemoteRecovery', {isAddressOnlyRecovery: false})
} catch (e: any) {
handleError(e)
}
diff --git a/src/services/keyChain.ts b/src/services/keyChain.ts
index 193ac02e..2fef9cdb 100644
--- a/src/services/keyChain.ts
+++ b/src/services/keyChain.ts
@@ -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 {
diff --git a/src/services/logService.ts b/src/services/logService.ts
index b657573b..53784a99 100644
--- a/src/services/logService.ts
+++ b/src/services/logService.ts
@@ -8,6 +8,8 @@ import * as Sentry from '@sentry/react-native'
import {rootStoreInstance} from '../models'
import {LogLevel} from './log/logTypes'
+// refresh
+
const {
userSettingsStore
} = rootStoreInstance
diff --git a/src/services/minibitsService.ts b/src/services/minibitsService.ts
index bc05e64c..e132e4b4 100644
--- a/src/services/minibitsService.ts
+++ b/src/services/minibitsService.ts
@@ -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,
}
\ No newline at end of file
diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts
index 15192fa5..5d90eafa 100644
--- a/src/services/nostrService.ts
+++ b/src/services/nostrService.ts
@@ -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()
diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts
index dc1e8062..e5145d00 100644
--- a/src/services/notificationService.ts
+++ b/src/services/notificationService.ts
@@ -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(
+ `⚡${formatCurrency(amount, currencyCode)} ${currencyCode} incoming!`,
+ `${zapSenderProfile ? 'Zap' : 'Payment'} from ${zapSenderProfile?.nip05 || 'unknown payer'} 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 {
@@ -90,8 +146,9 @@ const areNotificationsEnabled = async function (): Promise {
}
export const NotificationService = {
+ onReceiveRemoteNotification,
createLocalNotification,
- updateLocalNotification,
- cancelNotification,
+ // updateLocalNotification,
+ // cancelNotification,
areNotificationsEnabled
}
\ No newline at end of file
diff --git a/src/services/walletService.ts b/src/services/walletService.ts
index 9b8766d6..28ed0838 100644
--- a/src/services/walletService.ts
+++ b/src/services/walletService.ts
@@ -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
- handleInFlight: () => Promise
+ handleInFlight: () => Promise
handlePendingTopups: () => Promise
handlePendingTopup: (params: {
paymentRequest: PaymentRequest
}) => Promise
+ handleClaim: () => Promise
receiveEventsFromRelays: () => Promise
transfer: (
mintBalanceToTransferFrom: MintBalance,
@@ -971,6 +973,70 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment
}
+const handleClaim = async function (): Promise {
+
+ 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,
diff --git a/src/utils/envtypes/env.d.tsx b/src/utils/envtypes/env.d.tsx
index 59045585..f2670ab6 100644
--- a/src/utils/envtypes/env.d.tsx
+++ b/src/utils/envtypes/env.d.tsx
@@ -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
diff --git a/yarn.lock b/yarn.lock
index cb686f49..cafd58ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"