302 lines
12 KiB
JavaScript
302 lines
12 KiB
JavaScript
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
import { PERSISTED_LOGIN_COUNT_KEY } from '../../constants';
|
|
import { useGlobalContextProvider } from '../../../context-store/context';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import initializeUserSettingsFromHistory from '../../functions/initializeUserSettings';
|
|
import { useGlobalContactsInfo } from '../../../context-store/globalContacts';
|
|
import { useGlobalAppData } from '../../../context-store/appData';
|
|
import { GlobalThemeView } from '../../functions/CustomElements';
|
|
import LottieView from 'lottie-react-native';
|
|
import {
|
|
StackActions,
|
|
useNavigation,
|
|
useRoute,
|
|
} from '@react-navigation/native';
|
|
import { navigationRef } from '../../../navigation/navigationService';
|
|
import { useGlobalThemeContext } from '../../../context-store/theme';
|
|
import { useKeysContext } from '../../../context-store/keys';
|
|
import { updateMascatWalkingAnimation } from '../../functions/lottieViewColorTransformer';
|
|
import { crashlyticsLogReport } from '../../functions/crashlyticsLogs';
|
|
import { useSparkWallet } from '../../../context-store/sparkContext';
|
|
import { removeLocalStorageItem } from '../../functions/localStorage';
|
|
import { getAccountBalanceSnapshot } from '../../functions/spark/balanceSnapshots';
|
|
import { privateKeyFromSeedWords } from '../../functions/nostrCompatability';
|
|
import { getPublicKey } from 'nostr-tools';
|
|
import { useWebView } from '../../../context-store/webViewContext';
|
|
import ThemeIcon from '../../functions/CustomElements/themeIcon';
|
|
import { getCachedSparkTransactions } from '../../functions/spark';
|
|
import { deriveSparkIdentityKey } from '../../functions/gift/deriveGiftWallet';
|
|
import sha256Hash from '../../functions/hash';
|
|
import { useAppStatus } from '../../../context-store/appStatus';
|
|
import { initializeAllDatabases } from '../../functions/initializeAllDatabases';
|
|
import openWebBrowser from '../../functions/openWebBrowser';
|
|
import NoContentScreen from '../../functions/CustomElements/noContentScreen';
|
|
import { useNodeContext } from '../../../context-store/nodeContext';
|
|
import { getCachedFiatRate } from '../../functions/saveAndUpdateFiatData';
|
|
|
|
const mascotAnimation = require('../../assets/MOSCATWALKING.json');
|
|
|
|
export default function ConnectingToNodeLoadingScreen() {
|
|
const navigate = useNavigation();
|
|
const route = useRoute();
|
|
// Hash of the exact seed the caller intends this account to load. Threaded in
|
|
// from every entry point (create/restore via pin.js, relogin via pin/biometric,
|
|
// no-security via App.tsx initialParams). We refuse to derive the account
|
|
// identity until the in-context accountMnemoinc hashes to this value — see the
|
|
// gate in the connect effect below.
|
|
const expectedMnemonicHash = route.params?.expectedMnemonicHash;
|
|
const {
|
|
toggleMasterInfoObject,
|
|
masterInfoObject,
|
|
setMasterInfoObject,
|
|
preloadedUserData,
|
|
setPreLoadedUserData,
|
|
} = useGlobalContextProvider();
|
|
const { didRunHandshakeRef } = useWebView();
|
|
const { connectToSparkWallet, setSparkInformation } = useSparkWallet();
|
|
const { toggleContactsPrivateKey, accountMnemoinc } = useKeysContext();
|
|
const { theme } = useGlobalThemeContext();
|
|
const { toggleGlobalContactsInformation } = useGlobalContactsInfo();
|
|
const { toggleGlobalAppDataInformation } = useGlobalAppData();
|
|
const { screenDimensions } = useAppStatus();
|
|
const { toggleFiatStats } = useNodeContext();
|
|
const [hasError, setHasError] = useState(null);
|
|
const { t } = useTranslation();
|
|
const didRunConnectionRef = useRef(null);
|
|
|
|
const transformedAnimation = useMemo(
|
|
() =>
|
|
updateMascatWalkingAnimation(mascotAnimation, theme ? 'white' : 'blue'),
|
|
[theme],
|
|
);
|
|
|
|
useEffect(() => {
|
|
async function startConnectProcess() {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// A duplicate loading-screen instance can mount during the
|
|
// restore/login navigation race. If the app has already reached the
|
|
// home stack, this instance is stale — bail before re-running connect.
|
|
if (navigationRef.getCurrentRoute()?.name === 'HomeAdmin') return;
|
|
crashlyticsLogReport(
|
|
'Begining app connnection procress in loading screen',
|
|
);
|
|
removeLocalStorageItem(PERSISTED_LOGIN_COUNT_KEY);
|
|
|
|
// ── Phase 1: Derive keys + wait for webview handshake in parallel ──
|
|
const waitForHandshake = async () => {
|
|
if (didRunHandshakeRef.current) return;
|
|
console.warn('Webview has not finished setting up: wait here');
|
|
for (let i = 0; i < 10; i++) {
|
|
if (didRunHandshakeRef.current) break;
|
|
console.log('Waiting for webview to finish. Retry number:', i);
|
|
await new Promise(res => setTimeout(res, 1000));
|
|
}
|
|
};
|
|
|
|
// initializeAllDatabases is awaited here (it's fired non-blocking from
|
|
// the splash screen) so every local table exists before connecting or
|
|
// reading cached data. Runs in parallel with key derivation/handshake.
|
|
const [[privateKey, identityPubKey]] = await Promise.all([
|
|
Promise.all([
|
|
privateKeyFromSeedWords(accountMnemoinc),
|
|
deriveSparkIdentityKey(accountMnemoinc),
|
|
]),
|
|
waitForHandshake(),
|
|
initializeAllDatabases(),
|
|
]);
|
|
|
|
// Start wallet connection after keys are derived — passes identityPubKey
|
|
// so initializeSparkSession can skip getSparkBalance when snapshot exists
|
|
connectToSparkWallet(identityPubKey.publicKeyHex);
|
|
|
|
const publicKey = privateKey ? getPublicKey(privateKey) : null;
|
|
if (!privateKey || !publicKey)
|
|
throw new Error(
|
|
t('screens.inAccount.loadingScreen.userSettingsError'),
|
|
);
|
|
|
|
const hasSavedInfo = Object.keys(masterInfoObject || {}).length > 5;
|
|
|
|
// ── Phase 2: Cache reads + settings init all in parallel ──────────
|
|
const [placeholderTxs, balanceSnapshot, didLoadUserSettings] =
|
|
await Promise.all([
|
|
getCachedSparkTransactions(20, identityPubKey.publicKeyHex),
|
|
getAccountBalanceSnapshot(identityPubKey.publicKeyHex),
|
|
hasSavedInfo
|
|
? Promise.resolve(true)
|
|
: initializeUserSettingsFromHistory({
|
|
setMasterInfoObject,
|
|
toggleGlobalContactsInformation,
|
|
toggleGlobalAppDataInformation,
|
|
toggleMasterInfoObject,
|
|
preloadedData: preloadedUserData.data,
|
|
setPreLoadedUserData,
|
|
privateKey,
|
|
publicKey,
|
|
}),
|
|
]);
|
|
|
|
if (!hasSavedInfo) {
|
|
crashlyticsLogReport('Opened all SQL lite tables');
|
|
if (!didLoadUserSettings)
|
|
throw new Error(
|
|
t('screens.inAccount.loadingScreen.userSettingsError'),
|
|
);
|
|
crashlyticsLogReport('Loaded users settings from firebase');
|
|
}
|
|
|
|
// causes error in firebase let acync wait
|
|
//https://github.com/firebase/firebase-ios-sdk/issues/15974#issuecomment-4155423268
|
|
//https://github.com/firebase/firebase-ios-sdk/pull/15991
|
|
toggleContactsPrivateKey(privateKey);
|
|
console.log(balanceSnapshot, placeholderTxs, 'balance and tx snapshot');
|
|
|
|
// ── Phase 3: Apply cached balance ─────────────────────────────────
|
|
setSparkInformation(prev => ({
|
|
...prev,
|
|
transactions: placeholderTxs,
|
|
...(balanceSnapshot ?? {}),
|
|
}));
|
|
|
|
// Seed the fiat rate from cache so Home paints with a real rate
|
|
// instead of the placeholder while nodeContext fetches a fresh one.
|
|
const currency = masterInfoObject?.fiatCurrency || 'USD';
|
|
const cachedRate = await getCachedFiatRate(currency);
|
|
if (cachedRate?.fiatRate) toggleFiatStats(cachedRate.fiatRate);
|
|
|
|
// ── Phase 4: Minimum perceived loading time then navigate ─────────
|
|
const elapsed = Date.now() - startTime;
|
|
const minDuration = hasSavedInfo ? 500 : 1500;
|
|
await new Promise(resolve =>
|
|
setTimeout(resolve, Math.max(60, minDuration - elapsed)),
|
|
);
|
|
|
|
// Idempotent + dispatched through the container (not this instance's
|
|
// possibly-stale navigation prop): if a duplicate instance already
|
|
// moved us to HomeAdmin, skip — re-committing the screen is what throws
|
|
// "No view found for id … for fragment ScreenFragment" on Android.
|
|
if (
|
|
navigationRef.isReady() &&
|
|
navigationRef.getCurrentRoute()?.name !== 'HomeAdmin'
|
|
) {
|
|
navigationRef.dispatch(
|
|
StackActions.replace('HomeAdmin', { screen: 'Home' }),
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.log('intializatiion error', err);
|
|
if (err.message === 'dbInitError') {
|
|
setHasError({
|
|
title: t('screens.inAccount.loadingScreen.dbInitError1'),
|
|
subtitle: t('screens.inAccount.loadingScreen.dbInitError2'),
|
|
});
|
|
} else {
|
|
setHasError({
|
|
title: t('screens.inAccount.loadingScreen.initErrorTitle'),
|
|
subtitle: err.message,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (didRunConnectionRef.current) return;
|
|
if (preloadedUserData.isLoading && !preloadedUserData.data) return;
|
|
|
|
// ── Seed gate ─────────────────────────────────────────────────────────
|
|
// The account identity (UUID + spark identity) is derived from
|
|
// accountMnemoinc below. Under the restore/login navigation race this
|
|
// effect can run while accountMnemoinc is still empty/stale — deriving the
|
|
// identity from the wrong seed while the wallet connects with the correct
|
|
// one (real funds, wrong account). Latch the derivation only once the
|
|
// in-context seed is exactly the seed the caller intended. We do NOT set
|
|
// didRunConnectionRef here, so the effect re-runs (accountMnemoinc is a dep)
|
|
// and latches the instant the seed converges.
|
|
if (!accountMnemoinc) return;
|
|
if (expectedMnemonicHash) {
|
|
if (sha256Hash(accountMnemoinc) !== expectedMnemonicHash) return;
|
|
}
|
|
|
|
didRunConnectionRef.current = true;
|
|
|
|
requestAnimationFrame(startConnectProcess);
|
|
}, [
|
|
preloadedUserData,
|
|
masterInfoObject,
|
|
accountMnemoinc,
|
|
expectedMnemonicHash,
|
|
]);
|
|
|
|
// Safety valve for the seed gate: if the in-context seed never converges to
|
|
// the expected one (should not happen once the eager restore-path generation
|
|
// is fixed), surface the recoverable error UI instead of spinning forever.
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
if (!didRunConnectionRef.current) {
|
|
setHasError({
|
|
title: t('screens.inAccount.loadingScreen.initErrorTitle'),
|
|
subtitle: t('screens.inAccount.loadingScreen.userSettingsError'),
|
|
});
|
|
}
|
|
}, 30000);
|
|
return () => clearTimeout(timer);
|
|
}, [t]);
|
|
|
|
return (
|
|
<GlobalThemeView useStandardWidth={true}>
|
|
<View style={styles.globalContainer}>
|
|
{hasError ? (
|
|
<>
|
|
<TouchableOpacity
|
|
onPress={() =>
|
|
navigate.navigate('SettingsHome', { isDoomsday: true })
|
|
}
|
|
style={styles.doomsday}
|
|
>
|
|
<ThemeIcon iconName={'Settings'} />
|
|
</TouchableOpacity>
|
|
<NoContentScreen
|
|
iconName="TriangleAlert"
|
|
titleText={hasError.title}
|
|
subTitleText={hasError.subtitle}
|
|
showButton={true}
|
|
buttonText={t('constants.recover')}
|
|
buttonFunction={() =>
|
|
openWebBrowser({
|
|
navigate,
|
|
link: 'https://recover.blitzwalletapp.com/',
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
) : (
|
|
<LottieView
|
|
source={transformedAnimation}
|
|
autoPlay
|
|
loop={true}
|
|
style={{
|
|
width: Math.min(screenDimensions.width * 0.4, 400),
|
|
height: Math.min(screenDimensions.width * 0.4, 400),
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
</GlobalThemeView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
globalContainer: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
doomsday: {
|
|
position: 'absolute',
|
|
right: 0,
|
|
top: 0,
|
|
},
|
|
});
|