Skip to content

Commit

Permalink
Fix app crashes after autofocus of LNURL amount input. Define bugsnag…
Browse files Browse the repository at this point in the history
… component outside of app component to avoid subtree's DOM nodes and states beeing destroyed (#52)

* fix app crash after auto focus LNURL input

* Do not define components during render. React will destroy the entire subtree’s DOM nodes and state. Instead, move this component definition out of the parent component “App” and pass data as props.

* remove prop private from pkg.json

* update after review

* update after review

* update openUrl function (provisionally) #53

* update after review

* update wrong imports, update errorBoundary component. Fixes #54

* lint

* reset asset paths in app.config

* update after review

* Fixes #55 Minting sats should not allow decimal entry

* update after review
  • Loading branch information
KKA11010 committed Jun 14, 2023
1 parent 0dc9d3a commit 440d26a
Show file tree
Hide file tree
Showing 17 changed files with 182 additions and 216 deletions.
8 changes: 4 additions & 4 deletions .github/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ changelog:
- ignore-for-release
- bot
categories:
- title: Pull requests
- title: Bug fixes 🐛
labels:
- release
- bug
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: New Features 🎉
labels:
- enhancement
- title: Tests
- title: Tests 💯
labels:
- "test"
- title: Documentation
- title: Documentation 📄
labels:
- "documentation"
2 changes: 1 addition & 1 deletion app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ const configPath = `${__dirname}/config/app.config.ts`;

require('ts-node/register');

module.exports = require(configPath);
module.exports = require(configPath);
12 changes: 3 additions & 9 deletions config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { ExpoConfig } from 'expo/config'

import { version } from './../package.json'



type AppVariant = 'preview' | 'prod' | 'dev' | undefined

function nodeEnvShort(): 'test' | AppVariant {
Expand All @@ -17,6 +15,7 @@ function nodeEnvShort(): 'test' | AppVariant {
if (process.env.NODE_ENV === 'test') { return 'test' }
if (process.env.NODE_ENV === 'preview') { return 'preview' }
}

function appVariant(): AppVariant {
if (!process?.env?.APP_VARIANT) {
process.env.APP_VARIANT = 'dev'
Expand All @@ -27,7 +26,6 @@ function appVariant(): AppVariant {
if (process.env.APP_VARIANT === 'preview') { return 'preview' }
}


const _appVariant = appVariant() || process.env.APP_VARIANT || 'dev'

const _nodeEnvShort = nodeEnvShort()
Expand All @@ -36,15 +34,12 @@ try {
dotenvConfig({ path: `.env${_nodeEnvShort === 'prod' ? '' : `.${nodeEnvShort()}`}` })
} catch (e) { console.log('dotenv error:', e) } // eslint-disable-line no-console


// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const IS_DEV = _appVariant === 'dev'
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const IS_PREVIEW = _appVariant === 'preview'
const IS_PROD = _appVariant === 'prod'



const config: ExpoConfig = {
name: `eNuts${!IS_PROD ? ` (${_appVariant})` : ''}`,
slug: 'enuts',
Expand Down Expand Up @@ -74,7 +69,7 @@ const config: ExpoConfig = {
{
cameraPermission: 'Allow eNuts to access camera.'
}
]
],
],
ios: {
supportsTablet: true,
Expand Down Expand Up @@ -102,5 +97,4 @@ const config: ExpoConfig = {
}
}


export default config
export default config
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,4 @@
"url": "https://github.com/cashubtc/eNuts/issues"
},
"main": "src/AppEntry.ts"
}
}
12 changes: 0 additions & 12 deletions src/AppEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { l } from '@log'
import { setupReactotron } from '@log/reactotron'
import { registerRootComponent } from 'expo'


// Set up Reactotron, which is a free desktop app for inspecting and debugging
// React Native apps. Learn more here: https://github.com/infinitered/reactotron
if (isReactNativeDevMode) {
Expand All @@ -26,18 +25,7 @@ if (isReactNativeDevMode) {
})
}


// Bugsnag.notify(new Error('Test error from AppEntry'))

l('AppEntryPoint')



/* const x: IInitialProps = {
// expo?:,
exp: {},
mode: '\nDEV MODE\n'
} */

// eslint-disable-next-line new-cap
registerRootComponent(App)
164 changes: 75 additions & 89 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import { KeyboardProvider } from '@src/context/Keyboard'
import { ThemeContext } from '@src/context/Theme'
import { addToHistory } from '@store/HistoryStore'
import { dark, globals, light } from '@styles'
import { formatInt, hasTrustedMint, isCashuToken, sleep } from '@util'
import { formatInt, hasTrustedMint, isCashuToken, isErr, sleep } from '@util'
import { initCrashReporting } from '@util/crashReporting'
import { claimToken, isTokenSpendable, runRequestTokenLoop } from '@wallet'
import { getTokenInfo } from '@wallet/proofs'
import * as Clipboard from 'expo-clipboard'
import * as SplashScreen from 'expo-splash-screen'
import { StatusBar } from 'expo-status-bar'
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { AppState, Text, View } from 'react-native'

import { CustomErrorBoundary } from './ErrorScreen/ErrorBoundary'
Expand All @@ -33,59 +33,71 @@ import Txt from './Txt'

initCrashReporting()

const defaultPref: IPreferences = {
id: 1,
darkmode: false,
formatBalance: false,
theme: 'Default'
}

void SplashScreen.preventAutoHideAsync()

export default function App(_initialProps: IInitialProps) {
// Bugsnag Error boundary. docs: https://docs.bugsnag.com/platforms/javascript/react/
export default function App(initialProps: IInitialProps) {
if (env.BUGSNAG_API_KEY) {
// Create the error boundary...
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)
// Uses the bugsnack error boundary component which posts the errors to our bugsnag account
return (
<ErrorBoundary FallbackComponent={ErrorDetails}>
<_App _initialProps={initialProps} exp={initialProps.exp} />
</ErrorBoundary>
)
}
return (
<CustomErrorBoundary catchErrors='always'>
<_App _initialProps={initialProps} exp={initialProps.exp} />
</CustomErrorBoundary>
)
}

function _App(_initialProps: IInitialProps) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const [isRdy, setIsRdy] = useState(false)
const [claimed, setClaimed] = useState(false)
const claimData = { claimed, setClaimed }
const claimData = useMemo(() => ({ claimed, setClaimed }), [claimed])
// theme related
const [pref, setPref] = useState<IPreferences | undefined>()
const [theme, setTheme] = useState('Light')
const [color, setColors] = useState(theme === 'Light' ? light.custom : dark.custom)
const [highlight, setHighlight] = useState('Default')
const themeData = { pref, theme, setTheme, color, highlight, setHighlight }
// eslint-disable-next-line react-hooks/exhaustive-deps
const themeData = useMemo(() => ({ pref, theme, setTheme, color, highlight, setHighlight }), [pref])
// address book
const [contacts, setContacts] = useState<IContact[]>([])
const hasOwnAddress = () => contacts.some(c => c.isOwner)
const getPersonalInfo = () => contacts.find(c => c.isOwner)
const contactData = { contacts, setContacts, hasOwnAddress, getPersonalInfo }
// eslint-disable-next-line react-hooks/exhaustive-deps
const contactData = useMemo(() => ({ contacts, setContacts, hasOwnAddress, getPersonalInfo }), [contacts])
// app foregorund, background
const appState = useRef(AppState.currentState)
const [, setAppStateVisible] = useState(appState.current)
const [tokenInfo, setTokenInfo] = useState<ITokenInfo | undefined>()
const [claimOpen, setClaimOpen] = useState(false)
const { prompt, openPromptAutoClose } = usePrompt()

const handleForeground = async () => {
// TODO immediatly reading clipboard after the app comes to the foreground can result
// in an empty string returned. Find a better way than the following function to handle it.
let count = 0
let isSpent = false
const fn = async () => {
count++
const clipboard = await Clipboard.getStringAsync()
l({ clipboard, count })
if (!isCashuToken(clipboard)) { return false }
const info = getTokenInfo(clipboard)
if (!info) { return false }
// check if mint is a trusted one
const userMints = await getMintsUrls()
// do not claim from clipboard when app comes to the foreground if mint from token is not trusted
// TODO token can belong to multiple mints
if (!hasTrustedMint(userMints, info?.mints || [])) { return false }
if (!hasTrustedMint(userMints, info.mints)) { return false }
// check if token is spendable
const isSpendable = await isTokenSpendable(clipboard)
isSpent = !isSpendable
if (!info || !isSpendable) { return false }
if (!isSpendable) { return false }
setTokenInfo(info)
l('open modal!')
setClaimOpen(true)
return true
}
Expand Down Expand Up @@ -142,7 +154,7 @@ export default function App(_initialProps: IInitialProps) {
void setPreferences({ ...pref, theme: highlight })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlight])

// init
useEffect(() => {
async function initDB() {
try {
Expand All @@ -154,7 +166,7 @@ export default function App(_initialProps: IInitialProps) {
l(await getBalancesByKeysetId()) */
} catch (e) {
l(e)
alert(`Something went wrong while initializing the DB! ${e instanceof Error ? e.message : ''}`)
alert(`Something went wrong while initializing the DB! ${isErr(e) ? e.message : ''}`)
}
}
async function initPreferences() {
Expand All @@ -166,7 +178,12 @@ export default function App(_initialProps: IInitialProps) {
setHighlight(prefsDB?.theme || 'Default')
} catch (e) {
l(e)
setPref(defaultPref)
setPref({
id: 1,
darkmode: false,
formatBalance: false,
theme: 'Default'
})
} finally {
await SplashScreen.hideAsync()
}
Expand Down Expand Up @@ -201,91 +218,60 @@ export default function App(_initialProps: IInitialProps) {
void init().then(fsInfo)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const subscription = AppState.addEventListener('change', async nextAppState => {
setClaimed(false)
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
l('App has come to the foreground!')
setClaimed(false)
// check for clipboard valid cashu token when the app comes to the foregorund
await handleForeground()
}
appState.current = nextAppState
setAppStateVisible(appState.current)
l('AppState', appState.current)
})
return () => subscription.remove()
}, [])

if (!isRdy) { return null }

// Bugsnag Error boundary. docs: https://docs.bugsnag.com/platforms/javascript/react/
const BugSnagErrorBoundary = ({ children }: { children: React.ReactNode }) => {
if (env.BUGSNAG_API_KEY) {
// Create the error boundary...
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)
// Uses the bugsnack error boundary component which posts the errors to our bugsnag account
return (
<ErrorBoundary FallbackComponent={ErrorDetails}>
{children}
</ErrorBoundary>
)
}
return (
<CustomErrorBoundary catchErrors='always'>
{children}
</CustomErrorBoundary>
)
}

return (
<ThemeContext.Provider value={themeData}>
<NavigationContainer theme={theme === 'Light' ? light : dark}>
<BugSnagErrorBoundary>
<FocusClaimCtx.Provider value={claimData}>
<ContactsContext.Provider value={contactData}>
<KeyboardProvider>
<DrawerNav />
<StatusBar style="auto" />
{/* claim token if app comes to foreground and clipboard has valid cashu token */}
{claimOpen &&
<MyModal type='question' visible>
<Text style={globals(color, highlight).modalHeader}>
Found a cashu token in your clipboard
</Text>
<Text style={globals(color, highlight).modalTxt}>
Memo: {tokenInfo?.decoded.memo}{'\n'}
<Txt
txt={formatInt(tokenInfo?.value || 0)}
styles={[{ fontWeight: '500' }]}
/>
{' '}Satoshi from the following mint:{' '}
{tokenInfo?.mints.join(', ')}
</Text>
<Button
txt='Claim now!'
onPress={() => void handleRedeem()}
/>
<View style={{ marginVertical: 10 }} />
<Button
txt='Cancel'
outlined
onPress={() => setClaimOpen(false)}
/>
</MyModal>
}
{prompt.open &&
<Toaster
success
txt={prompt.msg}
<FocusClaimCtx.Provider value={claimData}>
<ContactsContext.Provider value={contactData}>
<KeyboardProvider>
<DrawerNav />
<StatusBar style="auto" />
{/* claim token if app comes to foreground and clipboard has valid cashu token */}
<MyModal type='question' visible={claimOpen}>
<Text style={globals(color, highlight).modalHeader}>
Found a cashu token in your clipboard
</Text>
<Text style={globals(color, highlight).modalTxt}>
Memo: {tokenInfo?.decoded.memo}{'\n'}
<Txt
txt={formatInt(tokenInfo?.value ?? 0)}
styles={[{ fontWeight: '500' }]}
/>
}
</KeyboardProvider>
</ContactsContext.Provider>
</FocusClaimCtx.Provider>
</BugSnagErrorBoundary>
{' '}Satoshi from the following mint:{' '}
{tokenInfo?.mints.join(', ')}
</Text>
<Button
txt='Claim now!'
onPress={() => void handleRedeem()}
/>
<View style={{ marginVertical: 10 }} />
<Button
txt='Cancel'
outlined
onPress={() => setClaimOpen(false)}
/>
</MyModal>
{prompt.open && <Toaster success txt={prompt.msg} />}
</KeyboardProvider>
</ContactsContext.Provider>
</FocusClaimCtx.Provider>
</NavigationContainer>
</ThemeContext.Provider>
)
}
}
Loading

0 comments on commit 440d26a

Please sign in to comment.