777 lines
28 KiB
TypeScript
777 lines
28 KiB
TypeScript
/**
|
|
* 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';
|
|
import {
|
|
crashlyticsLogReport,
|
|
crashlyticsRecordErrorReport,
|
|
} from './app/functions/crashlyticsLogs';
|
|
const DeepLinkIntentModule = NativeModules.DeepLinkIntentModule;
|
|
// Last URL handled via getInitialURL in this JS context. Belt-and-braces only:
|
|
// getInitialURL runs once per JS context, and this resets on a JS reload, so
|
|
// historical-intent suppression is owned by the native side
|
|
// (MainActivity.launchIntentIsHistorical). Deliberately not persisted: a stored
|
|
// value would swallow genuine re-taps of reusable links (lnurlp, bitcoin address).
|
|
let lastInitialUrl: string | null = null;
|
|
// Pending deep links older than this are discarded instead of replayed.
|
|
const PENDING_DEEP_LINK_MAX_AGE_MS = 10 * 60 * 1000;
|
|
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 (
|
|
<GestureHandlerRootView>
|
|
<SafeAreaProvider>
|
|
<InsetsProvider>
|
|
<KeyboardProvider>
|
|
<ToastProvider>
|
|
<GlobalThemeProvider>
|
|
<DropdownProvider>
|
|
<AppStatusProvider>
|
|
<AuthStatusProvider>
|
|
<KeysContextProvider>
|
|
<BTCMapProvider>
|
|
<GlobalContactsList>
|
|
<GlobalContextProvider>
|
|
<ActiveCustodyAccountProvider>
|
|
<WebViewProvider>
|
|
<SparkWalletProvider>
|
|
<GLobalNodeContextProider>
|
|
<GlobalAppDataProvider>
|
|
<PushNotificationProvider>
|
|
<LiquidEventProvider>
|
|
<RootstockSwapProvider>
|
|
<GlobalNostrWalletConnectProvider>
|
|
<ImageCacheProvider>
|
|
<GlobalServerTimeProvider>
|
|
<FlashnetProvider>
|
|
<UserBalanceProvider>
|
|
<AnalyticsNumbersProvider>
|
|
<SpendAndReplaceProvider>
|
|
{/* <Suspense
|
|
fallback={<FullLoadingScreen text={'Loading Page'} />}> */}
|
|
<ResetStack />
|
|
</SpendAndReplaceProvider>
|
|
</AnalyticsNumbersProvider>
|
|
</UserBalanceProvider>
|
|
</FlashnetProvider>
|
|
{/* </Suspense> */}
|
|
</GlobalServerTimeProvider>
|
|
</ImageCacheProvider>
|
|
</GlobalNostrWalletConnectProvider>
|
|
</RootstockSwapProvider>
|
|
</LiquidEventProvider>
|
|
</PushNotificationProvider>
|
|
</GlobalAppDataProvider>
|
|
</GLobalNodeContextProider>
|
|
</SparkWalletProvider>
|
|
</WebViewProvider>
|
|
</ActiveCustodyAccountProvider>
|
|
</GlobalContextProvider>
|
|
</GlobalContactsList>
|
|
</BTCMapProvider>
|
|
</KeysContextProvider>
|
|
</AuthStatusProvider>
|
|
</AppStatusProvider>
|
|
</DropdownProvider>
|
|
</GlobalThemeProvider>
|
|
</ToastProvider>
|
|
</KeyboardProvider>
|
|
</InsetsProvider>
|
|
</SafeAreaProvider>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|
|
|
|
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<any>(null);
|
|
const [linkTrigger, setLinkTrigger] = useState(0);
|
|
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) {
|
|
// Suppress Android relaunches from Recents, which redeliver the
|
|
// original VIEW intent. A genuine re-tap of the same (possibly
|
|
// reusable — lnurlp, bitcoin address) link launches without the
|
|
// history flag, so it still goes through. Read before clearIntent.
|
|
let launchedFromHistory = false;
|
|
if (
|
|
Platform.OS === 'android' &&
|
|
DeepLinkIntentModule?.isLaunchedFromHistory
|
|
) {
|
|
launchedFromHistory =
|
|
await DeepLinkIntentModule.isLaunchedFromHistory();
|
|
}
|
|
|
|
if (Platform.OS === 'android' && DeepLinkIntentModule?.clearIntent) {
|
|
DeepLinkIntentModule.clearIntent();
|
|
}
|
|
|
|
// In-memory guard against duplicate delivery of the launch URL
|
|
// within this JS context (clearIntent covers Android natively).
|
|
if (launchedFromHistory || lastInitialUrl === url) {
|
|
console.log('Deep link already handled:', url);
|
|
return;
|
|
}
|
|
lastInitialUrl = url;
|
|
}
|
|
|
|
console.log(
|
|
`[deeplink] handleDeepLink entry url=${url} isInitialLoad=${isInitialLoad}`,
|
|
);
|
|
console.log('Deep link URL:', url);
|
|
const linkData = {
|
|
url: event.url,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
await setLocalStorageItem(
|
|
'pendingDeepLinkData',
|
|
JSON.stringify(linkData),
|
|
);
|
|
setLinkTrigger(t => t + 1);
|
|
console.log(
|
|
`[deeplink] stored pendingDeepLinkData + bumped trigger url=${url}`,
|
|
);
|
|
} catch (error) {
|
|
console.error('Error handling deep link:', error);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
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;
|
|
|
|
if (!navigationRef.current) return;
|
|
if (appState !== 'active') return;
|
|
if (!didGetToHomepage || !publicKey) return;
|
|
|
|
const stored = await getLocalStorageItem('pendingDeepLinkData');
|
|
if (cancelled || !stored) return;
|
|
|
|
let parsed: { url?: string; timestamp?: number } | null = null;
|
|
try {
|
|
parsed = JSON.parse(stored);
|
|
} catch {
|
|
// Corrupt value would otherwise throw on every run — drop it.
|
|
await removeLocalStorageItem('pendingDeepLinkData');
|
|
return;
|
|
}
|
|
const { url, timestamp } = parsed || {};
|
|
if (!url) return;
|
|
|
|
// Discard stale links (e.g. tapped while locked and abandoned) instead
|
|
// of replaying a long-expired invoice after a much later unlock.
|
|
if (!timestamp || Date.now() - timestamp > PENDING_DEEP_LINK_MAX_AGE_MS) {
|
|
console.log(`[deeplink] discarding stale pending link url=${url}`);
|
|
await removeLocalStorageItem('pendingDeepLinkData');
|
|
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' ||
|
|
rootState.routes[0]?.name === 'SplashReload';
|
|
|
|
console.log(
|
|
`[deeplink] processing gate url=${url} didGetToHomepage=${didGetToHomepage} hasPublicKey=${!!publicKey} appState=${appState} navReady=${!!navigationRef.current} blockSoftReset=${blockSoftReset}`,
|
|
);
|
|
|
|
if (blockSoftReset) {
|
|
console.log(
|
|
`[deeplink] early-return blockSoftReset, keeping stored link url=${url}`,
|
|
);
|
|
}
|
|
|
|
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 },
|
|
});
|
|
// A newer run may have started during the network await; let it
|
|
// own the (still stored) link instead of double-processing.
|
|
if (cancelled) return;
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Consume the pending link after successful processing
|
|
await removeLocalStorageItem('pendingDeepLinkData');
|
|
console.log(`[deeplink] consumed pendingDeepLinkData url=${url}`);
|
|
}
|
|
} catch (err) {
|
|
// A cancelled run must not navigate or consume the link a newer run
|
|
// may be about to own (e.g. getDeepLinkUser rejecting after cleanup).
|
|
if (cancelled) return;
|
|
console.error('Error processing deep link:', err);
|
|
navigationRef.current.navigate('ErrorScreen', {
|
|
errorMessage: 'errormessages.processingDeepLinkError',
|
|
useTranslationString: true,
|
|
});
|
|
|
|
// Consume the pending link even if there was an error
|
|
await removeLocalStorageItem('pendingDeepLinkData');
|
|
console.log(
|
|
`[deeplink] consumed pendingDeepLinkData after error url=${url}`,
|
|
);
|
|
}
|
|
}, 700);
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
};
|
|
}, [linkTrigger, appState, didGetToHomepage, publicKey]);
|
|
|
|
useEffect(() => {
|
|
const subscription = Linking.addEventListener('url', handleDeepLink);
|
|
|
|
return () => {
|
|
subscription.remove();
|
|
};
|
|
}, [handleDeepLink]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function initWallet(skipURL = false) {
|
|
crashlyticsLogReport('initWallet: start');
|
|
await runPinAndMnemoicMigration();
|
|
await runSecureStoreMigrationV2();
|
|
crashlyticsLogReport('initWallet: secure store migrations done');
|
|
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;
|
|
}
|
|
}),
|
|
]);
|
|
|
|
crashlyticsLogReport('initWallet: read secure store + local settings');
|
|
|
|
// A corrupt value here would otherwise throw and strand the app on the
|
|
// native splash forever — fall back to the defaults below instead.
|
|
let storedSettings = null;
|
|
try {
|
|
storedSettings = JSON.parse(securitySettings);
|
|
} catch {
|
|
console.log('Corrupt stored security settings, using defaults');
|
|
}
|
|
|
|
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;
|
|
|
|
// initWallet is the ONLY thing that sets isLoaded, and the render gate below
|
|
// returns null until it does. Because preventAutoHideAsync() runs at module
|
|
// scope and only SplashScreen ever calls hideAsync(), a rejection here leaves
|
|
// the native splash on screen forever with no error and no way out. Always
|
|
// open the gate — landing on a screen is recoverable, an endless splash isn't.
|
|
const onInitFailure = (err: unknown) => {
|
|
console.log('initWallet error', err);
|
|
crashlyticsRecordErrorReport(
|
|
`initWallet failed: ${(err as Error)?.message}`,
|
|
);
|
|
setInitSettings(prev => ({ ...prev, isLoaded: true }));
|
|
};
|
|
|
|
if (!didInitializeSettings.current) {
|
|
didInitializeSettings.current = true;
|
|
initWallet(false).catch(onInitFailure);
|
|
} else {
|
|
didInitializeSettings.current = true;
|
|
initWallet(true).catch(onInitFailure);
|
|
}
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [appState]);
|
|
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 (
|
|
<NavigationContainer theme={navigationTheme} ref={navigationRef}>
|
|
{/* <StatusBar style={theme ? 'light' : 'dark'} translucent={true} /> */}
|
|
<HandleLNURLPayments />
|
|
<ToastContainer />
|
|
<SparkConnectionManager />
|
|
<Stack.Navigator screenOptions={screenOptions}>
|
|
<Stack.Screen
|
|
name="Splash"
|
|
component={SplashScreen}
|
|
options={{ animation: 'fade', gestureEnabled: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="SplashReload"
|
|
component={SplashScreen}
|
|
options={{ animation: 'none', gestureEnabled: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="Home"
|
|
component={HomeComponent}
|
|
initialParams={securitySettings}
|
|
options={{
|
|
animation: 'fade',
|
|
gestureEnabled: false,
|
|
contentStyle: {
|
|
backgroundColor: backgroundColor,
|
|
backfaceVisibility: 'hidden',
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ConnectingToNodeLoadingScreen"
|
|
component={ConnectingToNodeLoadingScreen}
|
|
options={{
|
|
gestureEnabled: false,
|
|
animation: 'fade',
|
|
contentStyle: {
|
|
backgroundColor: backgroundColor,
|
|
backfaceVisibility: 'hidden',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<Stack.Group
|
|
screenOptions={{
|
|
presentation: 'containedTransparentModal',
|
|
animation: 'slide_from_bottom',
|
|
}}
|
|
>
|
|
{SLIDE_FROM_BOTTOM_SCREENS.map(({ name, component: Component }) => (
|
|
<Stack.Screen
|
|
key={name}
|
|
name={name}
|
|
component={Component as React.ComponentType<any>}
|
|
/>
|
|
))}
|
|
</Stack.Group>
|
|
<Stack.Group
|
|
screenOptions={{
|
|
animation: 'slide_from_right',
|
|
presentation: 'card',
|
|
}}
|
|
>
|
|
{SLIDE_FROM_RIGHT_SCREENS.map(
|
|
({ name, component: Component, options = {} }) => (
|
|
<Stack.Screen
|
|
key={name}
|
|
name={name}
|
|
component={Component as React.ComponentType<any>}
|
|
options={{ ...options }}
|
|
/>
|
|
),
|
|
)}
|
|
</Stack.Group>
|
|
<Stack.Group
|
|
screenOptions={{
|
|
animation: 'fade',
|
|
presentation: 'containedTransparentModal',
|
|
}}
|
|
>
|
|
{FADE_SCREENS.map(({ name, component: Component, options = {} }) => (
|
|
<Stack.Screen
|
|
key={name}
|
|
name={name}
|
|
options={{ ...options }}
|
|
component={Component as React.ComponentType<any>}
|
|
/>
|
|
))}
|
|
</Stack.Group>
|
|
{/* <Stack.Group
|
|
screenOptions={{
|
|
animation: 'fade',
|
|
presentation: 'transparentModal',
|
|
}}
|
|
>
|
|
{FADE_TRANSPARENT_MODAL_SCREENS.map(
|
|
({ name, component: Component }) => (
|
|
<Stack.Screen
|
|
key={name}
|
|
name={name}
|
|
component={Component as React.ComponentType<any>}
|
|
/>
|
|
),
|
|
)}
|
|
</Stack.Group> */}
|
|
<Stack.Group
|
|
screenOptions={{
|
|
presentation: 'modal',
|
|
}}
|
|
>
|
|
{MODAL_CARD_SCREENS.map(({ name, component: Component }) => (
|
|
<Stack.Screen
|
|
key={name}
|
|
name={name}
|
|
component={Component as React.ComponentType<any>}
|
|
/>
|
|
))}
|
|
</Stack.Group>
|
|
</Stack.Navigator>
|
|
</NavigationContainer>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
registerRootComponent(App);
|