/** * Sample React Native App * https://github.com/facebook/react-native * * @format */ import { DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React, { JSX, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { registerRootComponent } from 'expo'; import { getLocalStorageItem, retrieveData, setLocalStorageItem, removeLocalStorageItem, } from './app/functions'; import { AdminLogin, ConnectingToNodeLoadingScreen, } from './app/screens/inAccount'; import { GlobalContextProvider } from './context-store/context'; import { WebViewProvider } from './context-store/webViewContext'; import { Linking, Platform, NativeModules } from 'react-native'; import SplashScreen from './app/screens/splashScreen'; import sha256Hash from './app/functions/hash'; import { GlobalContactsList } from './context-store/globalContacts'; import { CreateAccountHome } from './app/screens/createAccount'; import { getLocales } from 'react-native-localize'; import { supportedLanguagesList } from './locales/localeslist'; import { GlobalAppDataProvider } from './context-store/appData'; import { PushNotificationProvider } from './context-store/notificationManager'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import GetThemeColors from './app/hooks/themeColors'; import { BLITZ_PAYMENT_DEEP_LINK_SCHEMES, CONTACT_UNIVERSAL_LINK_REGEX, GIFT_DEEPLINK_REGEX, POOL_DEEPLINK_REGEX, PAYLINK_DEEPLINK_REGEX, LOGIN_SECUITY_MODE_KEY, LOGIN_SECURITY_MODE_TYPE_KEY, } from './app/constants'; import { LiquidEventProvider } from './context-store/liquidEventContext'; import { GlobalThemeProvider, useGlobalThemeContext, } from './context-store/theme'; import { GLobalNodeContextProider } from './context-store/nodeContext'; import { AppStatusProvider, useAppStatus } from './context-store/appStatus'; import { KeysContextProvider, useKeysContext } from './context-store/keys'; import { FADE_SCREENS, // FADE_TRANSPARENT_MODAL_SCREENS, MODAL_CARD_SCREENS, SLIDE_FROM_BOTTOM_SCREENS, SLIDE_FROM_RIGHT_SCREENS, } from './navigation/screens'; import getDeepLinkUser from './app/components/admin/homeComponents/contacts/internalComponents/getDeepLinkUser'; import { navigationRef } from './navigation/navigationService'; import { ImageCacheProvider } from './context-store/imageCache'; import { runPinAndMnemoicMigration, runSecureStoreMigrationV2, } from './app/functions/secureStore'; import { KeyboardProvider } from 'react-native-keyboard-controller'; import HandleLNURLPayments from './context-store/lnurl'; import { SparkWalletProvider } from './context-store/sparkContext'; import { DropdownProvider } from './context-store/dropdownContext'; import * as ExpoSplashScreen from 'expo-splash-screen'; import * as NavigationBar from 'expo-navigation-bar'; import { setStatusBarBackgroundColor, setStatusBarStyle, } from 'expo-status-bar'; import * as SystemUI from 'expo-system-ui'; import { InsetsProvider } from './context-store/insetsProvider'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { ToastProvider } from './context-store/toastManager'; import { ToastContainer } from './context-store/toastContainer'; import { RootstockSwapProvider } from './context-store/rootstockSwapContext'; import { SparkConnectionManager } from './context-store/sparkConnection'; import { GlobalNostrWalletConnectProvider } from './context-store/NWC'; import { GlobalServerTimeProvider } from './context-store/serverTime'; import { AuthStatusProvider } from './context-store/authContext'; import { ActiveCustodyAccountProvider } from './context-store/activeAccount'; import { UserBalanceProvider } from './context-store/userBalanceContext'; import { FlashnetProvider } from './context-store/flashnetContext'; import { useTranslation } from 'react-i18next'; import { AnalyticsNumbersProvider } from './context-store/analyticsContext'; import { BTCMapProvider } from './context-store/btcMapContext'; import { SpendAndReplaceProvider } from './context-store/spendAndReplaceContext'; const DeepLinkIntentModule = NativeModules.DeepLinkIntentModule; const Stack = createNativeStackNavigator(); // will unhide splashscreen when showing dynamic loading in splashscreen component ExpoSplashScreen.preventAutoHideAsync() .then(result => console.log(`SplashScreen.preventAutoHideAsync() succeeded: ${result}`), ) .catch(console.warn); function App(): JSX.Element { return ( {/* }> */} {/* */} ); } function ResetStack(): JSX.Element | null { const [initSettings, setInitSettings] = useState<{ isLoggedIn: boolean | null; hasSecurityEnabled: boolean | null; isLoaded: boolean | null; }>({ isLoggedIn: null, hasSecurityEnabled: null, isLoaded: null, }); const [securitySettings, setSecuritySettings] = useState(null); const [pendingLinkData, setPendingLinkData] = useState<{ url: string; timestamp: number | null; }>({ url: '', timestamp: null, }); const { theme, darkModeType } = useGlobalThemeContext(); const { didGetToHomepage, appState } = useAppStatus(); const { publicKey, setAccountMnemonic } = useKeysContext(); const didInitializeSettings = useRef(false); const { backgroundColor } = GetThemeColors(); const { i18n } = useTranslation(); const handleDeepLink = useCallback( async (event: { url: string }, isInitialLoad = false) => { console.log(event); const { url } = event; try { if (isInitialLoad) { const savedDeepLink = await getLocalStorageItem( 'lastHandledDeepLink', ); const parsedSavedDeeplink = JSON.parse(savedDeepLink) || { url: '', dateAdded: null, }; if ( Platform.OS === 'android' && DeepLinkIntentModule && DeepLinkIntentModule.clearIntent ) { DeepLinkIntentModule.clearIntent(); } if (parsedSavedDeeplink.url === url) { console.log('Deep link already handled:', url); return; } await setLocalStorageItem( 'lastHandledDeepLink', JSON.stringify({ url: url, dateAdded: Date.now() }), ); } console.log('Deep link URL:', url); const linkData = { url: event.url, timestamp: Date.now(), }; setPendingLinkData(linkData); await setLocalStorageItem( 'pendingDeepLinkData', JSON.stringify(linkData), ); console.log('Stored pending link in state AND AsyncStorage'); } catch (error) { console.error('Error handling deep link:', error); } }, [setPendingLinkData], ); const clearDeepLink = useCallback(async () => { setPendingLinkData({ url: '', timestamp: null, }); await removeLocalStorageItem('pendingDeepLinkData'); }, []); const getInitialURL = useCallback(async () => { const url = await Linking.getInitialURL(); if (url) { handleDeepLink({ url }, true); console.log('Initial deep link stored:', url); } }, [handleDeepLink]); const setNavigationBar = useCallback(async () => { if (appState === 'active') { try { if (Platform.OS === 'android') { await NavigationBar.setBackgroundColorAsync(backgroundColor); await NavigationBar.setButtonStyleAsync(theme ? 'light' : 'dark'); setStatusBarBackgroundColor(backgroundColor, false); setStatusBarStyle(theme ? 'light' : 'dark', false); } await SystemUI.setBackgroundColorAsync(backgroundColor); } catch (error) { console.warn('Failed to set navigation bar:', error); } } }, [backgroundColor, theme, appState]); useEffect(() => { let debounceTimer: NodeJS.Timeout | null = null; let cancelled = false; debounceTimer = setTimeout(async () => { if (cancelled) return; const { url, timestamp } = pendingLinkData; if (!url) return; if (!navigationRef.current) return; if (appState !== 'active') return; if (!didGetToHomepage || !publicKey) return; try { // Convert URL to lowercase for case-insensitive checks const lowerUrl = url.toLowerCase(); const contactSchemePrefix = 'blitz-wallet:'; console.log( 'Processing link:', url, 'at timestamp:', timestamp, 'conditions:', { didGetToHomepage, hasNavigationRef: !!navigationRef.current, hasPublicKey: !!publicKey, }, ); const rootState = navigationRef?.getRootState() ?? { routes: [] }; const blockSoftReset = (rootState.routes[0]?.name === 'Home' && rootState.routes.length === 1) || rootState.routes[0]?.name === 'Splash'; if (!blockSoftReset) { let isContactLink = false; if (PAYLINK_DEEPLINK_REGEX.test(url)) { const match = url.match(/paylink\/([A-Za-z0-9]{9})/i); if (match) { navigationRef.current.reset({ index: 0, routes: [ { name: 'HomeAdmin', params: { screen: 'Home' }, }, { name: 'ConfirmPaymentScreen', params: { btcAdress: `paylink://${match[1]}`, fromPage: 'paylink', }, }, ], }); } } else if (POOL_DEEPLINK_REGEX.test(url)) { const poolIdMatch = url.match(/pools\/([0-9a-f-]{36})/i); if (poolIdMatch) { navigationRef.current.navigate('PoolsStack', { screen: 'PoolDetailScreen', params: { poolId: poolIdMatch[1] }, }); } } else if (GIFT_DEEPLINK_REGEX.test(url)) { navigationRef.current.navigate('CustomHalfModal', { wantedContent: 'ClaimGiftScreen', url, sliderHight: 0.6, claimType: 'claim', }); } else { if (CONTACT_UNIVERSAL_LINK_REGEX.test(url)) { isContactLink = true; } if (lowerUrl.startsWith(contactSchemePrefix)) { // If the URL starts with the contact scheme, check if it contains a wrapped payment scheme. const contentAfterScheme = lowerUrl.substring( contactSchemePrefix.length, ); const isWrappedPaymentLink = BLITZ_PAYMENT_DEEP_LINK_SCHEMES.some( scheme => // Check if the content starts with "scheme:" (e.g., "lightning:") contentAfterScheme.startsWith(scheme + ':'), ); isContactLink = !isWrappedPaymentLink; } if (isContactLink) { // Logic for handling contact deep links const deepLinkContact = await getDeepLinkUser({ deepLinkContent: url, userProfile: { uuid: publicKey }, }); if (deepLinkContact.didWork) { navigationRef.current.navigate('ExpandedAddContactsPage', { newContact: deepLinkContact.data, }); } else { navigationRef.current.navigate('ErrorScreen', { errorMessage: deepLinkContact.reason, useTranslationString: true, }); } } else { // Regex to strip 'blitz-wallet:' OR 'blitz:' prefix if it exists. // This ensures only the core payment URI is passed to the ConfirmPaymentScreen. const paymentUrl = url.replace(/^(blitz-wallet|blitz):/i, ''); navigationRef.current.navigate('ConfirmPaymentScreen', { btcAdress: paymentUrl, }); } } // Clear the pending link after successful processing await clearDeepLink(); } } catch (err) { console.error('Error processing deep link:', err); navigationRef.current.navigate('ErrorScreen', { errorMessage: 'errormessages.processingDeepLinkError', useTranslationString: true, }); // Clear the pending link even if there was an error await clearDeepLink(); } }, 700); return () => { cancelled = true; if (debounceTimer) clearTimeout(debounceTimer); }; }, [pendingLinkData, appState, didGetToHomepage, publicKey, clearDeepLink]); useEffect(() => { async function loadPendingDeepLink() { try { const stored = await getLocalStorageItem('pendingDeepLinkData'); if (stored) { const parsed = JSON.parse(stored); console.log('Loaded pending deep link from AsyncStorage:', parsed); setPendingLinkData(parsed); } } catch (error) { console.error('Error loading pending deep link:', error); } } loadPendingDeepLink(); }, []); useEffect(() => { const subscription = Linking.addEventListener('url', handleDeepLink); return () => { subscription.remove(); }; }, [handleDeepLink]); useEffect(() => { let cancelled = false; async function initWallet(skipURL = false) { await runPinAndMnemoicMigration(); await runSecureStoreMigrationV2(); const [ initialURL, loginModeType, pin, mnemonic, securitySettings, userSelectedLanguage, ] = await Promise.all([ skipURL ? Promise.resolve() : getInitialURL(), retrieveData(LOGIN_SECURITY_MODE_TYPE_KEY), retrieveData('pinHash'), retrieveData('encryptedMnemonic'), getLocalStorageItem(LOGIN_SECUITY_MODE_KEY), getLocalStorageItem('userSelectedLanguage').then(data => { try { return JSON.parse(data); } catch { return null; } }), ]); const storedSettings = JSON.parse(securitySettings); const isPinFromMode = loginModeType?.value === 'pin'; const isBiometricFromMode = loginModeType?.value === 'biometric'; const parsedSettings = storedSettings ?? { isSecurityEnabled: true, isPinEnabled: isPinFromMode || (!isPinFromMode && !isBiometricFromMode), isBiometricEnabled: isBiometricFromMode, }; if (!storedSettings) setLocalStorageItem( LOGIN_SECUITY_MODE_KEY, JSON.stringify(parsedSettings), ); if (cancelled) return; const isNoSecurityLogin = mnemonic.value && !parsedSettings.isSecurityEnabled; if (isNoSecurityLogin) { setAccountMnemonic(mnemonic.value); } // For the no-security path the loading screen renders directly as Home, so // thread the intended seed's hash through its initialParams (kept out of the // persisted security-settings object) to gate its identity derivation. setSecuritySettings( isNoSecurityLogin ? { ...parsedSettings, expectedMnemonicHash: sha256Hash(mnemonic.value) } : parsedSettings, ); setInitSettings(prev => { return { ...prev, isLoggedIn: !!pin.value && !!mnemonic.value, hasSecurityEnabled: parsedSettings.isSecurityEnabled, // Settings are now resolved — unblock the render gate below. Until this // is true the navigator stays unmounted so Home never mounts with the // wrong (still-loading) component. This is the login race-condition fix. isLoaded: true, }; }); let resolvedLanguage = userSelectedLanguage; if (!resolvedLanguage) { const [{ languageTag = 'en' }] = getLocales(); const deviceShortId = languageTag.split('-')[0]; const matched = supportedLanguagesList.find( l => l.shortId === deviceShortId, ); resolvedLanguage = matched ? matched.id : 'en'; setLocalStorageItem( 'userSelectedLanguage', JSON.stringify(resolvedLanguage), ); } i18n.changeLanguage(resolvedLanguage); } if (appState === 'background') return; if (!didInitializeSettings.current) { didInitializeSettings.current = true; initWallet(false); } else { didInitializeSettings.current = true; initWallet(true); } return () => { cancelled = true; }; }, [appState]); const handleAnimationFinish = () => { setInitSettings(prev => { return { ...prev, isLoaded: true }; }); }; const navigationTheme = useMemo( () => ({ ...DefaultTheme, dark: theme, colors: { ...DefaultTheme.colors, primary: '#1E1E1E', background: backgroundColor, card: '#1E1E1E', text: '#1E1E1E', border: '#1E1E1E', notification: '#1E1E1E', }, }), [theme, backgroundColor], ); useEffect(() => { if (appState === 'background') return; setNavigationBar(); }, [backgroundColor, theme, appState]); const screenOptions = useMemo(() => { return { headerShown: false, keyboardHandlingEnabled: true, }; }, []); const HomeComponent = useMemo(() => { if (initSettings.isLoggedIn) { return initSettings.hasSecurityEnabled ? AdminLogin : ConnectingToNodeLoadingScreen; } return CreateAccountHome; }, [initSettings.isLoggedIn, initSettings.hasSecurityEnabled]); if (theme === null || darkModeType === null || !initSettings.isLoaded) { return null; } if (appState === 'background' && !didInitializeSettings.current) return null; return ( {/* */} {SLIDE_FROM_BOTTOM_SCREENS.map(({ name, component: Component }) => ( } /> ))} {SLIDE_FROM_RIGHT_SCREENS.map( ({ name, component: Component, options = {} }) => ( } options={{ ...options }} /> ), )} {FADE_SCREENS.map(({ name, component: Component, options = {} }) => ( } /> ))} {/* {FADE_TRANSPARENT_MODAL_SCREENS.map( ({ name, component: Component }) => ( } /> ), )} */} {MODAL_CARD_SCREENS.map(({ name, component: Component }) => ( } /> ))} ); } export default App; registerRootComponent(App);