diff --git a/eslint.config.mjs b/eslint.config.mjs index c3ee138c788..ad9c0204ab9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -91,14 +91,12 @@ export default [ 'src/actions/FioAddressActions.ts', 'src/actions/FirstOpenActions.tsx', 'src/actions/LoanWelcomeActions.tsx', - 'src/actions/LocalSettingsActions.ts', - 'src/actions/LoginActions.tsx', 'src/actions/NotificationActions.ts', 'src/actions/PaymentProtoActions.tsx', 'src/actions/ReceiveDropdown.tsx', 'src/actions/RecoveryReminderActions.tsx', - 'src/actions/RequestReviewActions.tsx', + 'src/actions/ScamWarningActions.tsx', 'src/actions/ScanActions.tsx', @@ -298,10 +296,9 @@ export default [ 'src/components/scenes/OtpSettingsScene.tsx', 'src/components/scenes/PasswordRecoveryScene.tsx', 'src/components/scenes/PromotionSettingsScene.tsx', - 'src/components/scenes/ReviewTriggerTestScene.tsx', + 'src/components/scenes/SecurityAlertsScene.tsx', - 'src/components/scenes/SettingsScene.tsx', 'src/components/scenes/SpendingLimitsScene.tsx', 'src/components/scenes/Staking/EarnScene.tsx', 'src/components/scenes/Staking/StakeOptionsScene.tsx', @@ -318,7 +315,7 @@ export default [ 'src/components/scenes/TransactionsExportScene.tsx', 'src/components/scenes/UpgradeUsernameScreen.tsx', - 'src/components/scenes/WalletListScene.tsx', + 'src/components/scenes/WalletRestoreScene.tsx', 'src/components/scenes/WcConnectionsScene.tsx', 'src/components/scenes/WcConnectScene.tsx', @@ -494,9 +491,7 @@ export default [ 'src/plugins/stake-plugins/util/builder.ts', 'src/reducers/ExchangeInfoReducer.ts', 'src/reducers/NetworkReducer.ts', - 'src/reducers/PasswordReminderReducer.ts', - 'src/reducers/SpendingLimitsReducer.ts', 'src/selectors/getCreateWalletList.ts', 'src/selectors/SettingsSelectors.ts', 'src/state/createStateProvider.tsx', @@ -515,7 +510,7 @@ export default [ 'src/util/CurrencyWalletHelpers.ts', 'src/util/exchangeRates.ts', - 'src/util/fake/FakeProviders.tsx', + 'src/util/FioAddressUtils.ts', 'src/util/getAccountUsername.ts', 'src/util/GuiPluginTools.ts', diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index 3caf6936468..c159991a518 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -113,10 +113,9 @@ exports[`initialState 1`] = ` "defaultFiat": "USD", "defaultIsoFiat": "iso:USD", "denominationSettings": {}, + "denominationSettingsOptimized": false, "developerModeOn": false, "isAccountBalanceVisible": true, - "isTouchEnabled": false, - "isTouchSupported": false, "mostRecentWallets": [], "notifState": {}, "passwordRecoveryRemindersShown": { @@ -135,7 +134,6 @@ exports[`initialState 1`] = ` "nonPasswordLoginsLimit": 4, "passwordUseCount": 0, }, - "pinLoginEnabled": false, "preferredSwapPluginId": undefined, "preferredSwapPluginType": undefined, "rampLastCryptoSelection": undefined, diff --git a/src/__tests__/spendingLimits.test.ts b/src/__tests__/spendingLimits.test.ts index f099f917c99..cb09a18808f 100644 --- a/src/__tests__/spendingLimits.test.ts +++ b/src/__tests__/spendingLimits.test.ts @@ -14,12 +14,14 @@ describe('spendingLimits', () => { describe('when logging in', () => { it('should update', () => { const actual = spendingLimits(initialState, { - type: 'ACCOUNT_INIT_COMPLETE', + type: 'LOGIN', data: { - spendingLimits: { - transaction: { - isEnabled: false, - amount: 150 + localSettings: { + spendingLimits: { + transaction: { + isEnabled: false, + amount: 150 + } } } } as any diff --git a/src/actions/LocalSettingsActions.ts b/src/actions/LocalSettingsActions.ts index b7b9ac2e199..e8b0986ad91 100644 --- a/src/actions/LocalSettingsActions.ts +++ b/src/actions/LocalSettingsActions.ts @@ -33,7 +33,7 @@ export const getLocalAccountSettings = async ( return settings } -export function useAccountSettings() { +export function useAccountSettings(): LocalAccountSettings { const [accountSettings, setAccountSettings] = React.useState(localAccountSettings) React.useEffect(() => watchAccountSettings(setAccountSettings), []) @@ -268,9 +268,12 @@ export const readLocalAccountSettings = async ( emitAccountSettings(settings) readSettingsFromDisk = true return settings - } catch (e) { + } catch (error: unknown) { + // If Settings.json doesn't exist yet, return defaults without writing. + // Defaults can be derived from cleaners. Only write when values change. const defaults = asLocalAccountSettings({}) - return await writeLocalAccountSettings(account, defaults) + emitAccountSettings(defaults) + return defaults } } diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 033ea132aea..a2e1071dd10 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -1,12 +1,6 @@ -import type { - EdgeAccount, - EdgeCreateCurrencyWallet, - EdgeTokenId -} from 'edge-core-js/types' +import type { EdgeAccount, EdgeCreateCurrencyWallet } from 'edge-core-js/types' import { - getSupportedBiometryType, hasSecurityAlerts, - isTouchEnabled, refreshTouchId, showNotificationPermissionReminder } from 'edge-login-ui-rn' @@ -16,7 +10,11 @@ import { getCurrencies } from 'react-native-localize' import performance from 'react-native-performance' import { sprintf } from 'sprintf-js' -import { readSyncedSettings } from '../actions/SettingsActions' +import { + migrateDenominationSettings, + readSyncedSettings, + type SyncedAccountSettings +} from '../actions/SettingsActions' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' import { FioCreateHandleModal } from '../components/modals/FioCreateHandleModal' import { SurveyModal } from '../components/modals/SurveyModal' @@ -24,13 +22,9 @@ import { Airship, showError } from '../components/services/AirshipInstance' import { ENV } from '../env' import { getExperimentConfig } from '../experimentConfig' import { lstrings } from '../locales/strings' -import { - type AccountInitPayload, - initialState -} from '../reducers/scenes/SettingsReducer' import type { WalletCreateItem } from '../selectors/getCreateWalletList' import { config } from '../theme/appConfig' -import type { Dispatch, ThunkAction } from '../types/reduxTypes' +import type { Dispatch, GetState, ThunkAction } from '../types/reduxTypes' import type { EdgeAppSceneProps, NavigationBase } from '../types/routerTypes' import { currencyCodesToEdgeAssets } from '../util/CurrencyInfoHelpers' import { logActivity } from '../util/logger' @@ -50,148 +44,56 @@ import { registerNotificationsV2, updateNotificationSettings } from './NotificationActions' -import { showScamWarningModal } from './ScamWarningActions' const PER_WALLET_TIMEOUT = 5000 const MIN_CREATE_WALLET_TIMEOUT = 20000 -function getFirstActiveWalletInfo(account: EdgeAccount): { - walletId: string - tokenId: EdgeTokenId -} { - // Find the first wallet: - const [walletId] = account.activeWalletIds - const walletKey = account.allKeys.find(key => key.id === walletId) - - // Find the matching currency code: - if (walletKey != null) { - for (const pluginId of Object.keys(account.currencyConfig)) { - const { currencyInfo } = account.currencyConfig[pluginId] - if (currencyInfo.walletType === walletKey.type) { - return { walletId, tokenId: null } - } - } - } - - // The user has no wallets: - return { walletId: '', tokenId: null } -} - export function initializeAccount( navigation: NavigationBase, account: EdgeAccount ): ThunkAction> { return async (dispatch, getState) => { - const rootNavigation = getRootNavigation(navigation) - - // Log in as quickly as possible, but we do need the sort order: - const syncedSettings = await readSyncedSettings(account) - const { walletsSort } = syncedSettings - dispatch({ type: 'LOGIN', data: { account, walletSort: walletsSort } }) const { newAccount } = account - const referralPromise = dispatch(loadAccountReferral(account)) - - // Track whether we showed a non-survey modal or some other interrupting UX. - // We don't want to pester the user with too many interrupting flows. - let hideSurvey = false - - if (newAccount) { - await referralPromise - let { defaultFiat } = syncedSettings - - const [phoneCurrency] = getCurrencies() - if (typeof phoneCurrency === 'string' && phoneCurrency.length >= 3) { - defaultFiat = phoneCurrency - } - // Ensure the creation reason is available before creating wallets: - const accountReferralCurrencyCodes = - getState().account.accountReferral.currencyCodes - const defaultSelection = - accountReferralCurrencyCodes != null - ? currencyCodesToEdgeAssets(account, accountReferralCurrencyCodes) - : config.defaultWallets - const fiatCurrencyCode = 'iso:' + defaultFiat - - // Ensure we have initialized the account settings first so we can begin - // keeping track of token warnings shown from the initial selected assets - // during account creation - await readLocalAccountSettings(account) - - const newAccountFlow = async ( - navigation: EdgeAppSceneProps< - 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' - >['navigation'], - items: WalletCreateItem[] - ) => { - navigation.replace('edgeTabs', { screen: 'home' }) - const createWalletsPromise = createCustomWallets( - account, - fiatCurrencyCode, - items, - dispatch - ).catch(error => { - showError(error) - }) - - // New user FIO handle registration flow (if env is properly configured) - const { freeRegApiToken = '', freeRegRefCode = '' } = - typeof ENV.FIO_INIT === 'object' ? ENV.FIO_INIT : {} - if (freeRegApiToken !== '' && freeRegRefCode !== '') { - hideSurvey = true - const isCreateHandle = await Airship.show(bridge => ( - - )) - if (isCreateHandle) { - navigation.navigate('fioCreateHandle', { - freeRegApiToken, - freeRegRefCode - }) - } - } + const rootNavigation = getRootNavigation(navigation) - await createWalletsPromise - dispatch( - logEvent('Signup_Complete', { - numAccounts: getState().core.context.localUsers.length - }) - ) + // Load all settings upfront so we can navigate immediately after LOGIN + const [syncedSettings, localSettings] = await Promise.all([ + readSyncedSettings(account), + readLocalAccountSettings(account) + ]) + + // Dispatch LOGIN with all settings - this enables immediate navigation + dispatch({ + type: 'LOGIN', + data: { + account, + syncedSettings, + localSettings } + }) - rootNavigation.replace('edgeApp', { - screen: 'edgeAppStack', - params: { - screen: 'createWalletSelectCryptoNewAccount', - params: { - newAccountFlow, - defaultSelection, - disableLegacy: true - } - } - }) + const referralPromise = dispatch(loadAccountReferral(account)) - performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + // Navigate immediately - all settings are now in Redux + if (newAccount) { + await navigateToNewAccountFlow( + rootNavigation, + account, + syncedSettings, + referralPromise, + dispatch, + getState + ) } else { - const { defaultScreen } = getDeviceSettings() - rootNavigation.replace('edgeApp', { - screen: 'edgeAppStack', - params: { - screen: 'edgeTabs', - params: - defaultScreen === 'home' - ? { screen: 'home' } - : { screen: 'walletsTab', params: { screen: 'walletList' } } - } - }) - referralPromise.catch(() => { - console.log(`Failed to load account referral info`) - }) - - performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + navigateToExistingAccountHome(rootNavigation, referralPromise) } + performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + + // Track whether we showed a non-survey modal or some other interrupting UX. + // We don't want to pester the user with too many interrupting flows. + let hideSurvey = false + // Show a notice for deprecated electrum server settings const pluginIdsNeedingUserAction: string[] = [] for (const pluginId in account.currencyConfig) { @@ -231,118 +133,59 @@ export function initializeAccount( }) } }) - .catch(err => { - showError(err) + .catch((error: unknown) => { + showError(error) }) } - // Show the scam warning modal if needed - if (await showScamWarningModal('firstLogin')) hideSurvey = true - // Check for security alerts: if (hasSecurityAlerts(account)) { navigation.push('securityAlerts') hideSurvey = true } - const state = getState() - const { context } = state.core - // Sign up for push notifications: - dispatch(registerNotificationsV2()).catch(e => { - console.error(e) + dispatch(registerNotificationsV2()).catch((error: unknown) => { + console.error(error) }) const walletInfos = account.allKeys const filteredWalletInfos = walletInfos.map(({ keys, id, ...info }) => info) console.log('Wallet Infos:', filteredWalletInfos) - // Merge and prepare settings files: - let accountInitObject: AccountInitPayload = { - ...initialState, - account, - tokenId: null, - pinLoginEnabled: false, - isTouchEnabled: await isTouchEnabled(account), - isTouchSupported: (await getSupportedBiometryType()) !== false, - walletId: '', - walletsSort: 'manual' - } - try { - if (!newAccount) { - // We have a wallet - const { walletId, tokenId } = getFirstActiveWalletInfo(account) - accountInitObject.walletId = walletId - accountInitObject.tokenId = tokenId + // Run one-time migration to clean up denomination settings in background + migrateDenominationSettings(account, syncedSettings).catch( + (error: unknown) => { + console.log('Failed to migrate denomination settings:', error) } + ) - accountInitObject = { ...accountInitObject, ...syncedSettings } - - const loadedLocalSettings = await readLocalAccountSettings(account) - accountInitObject = { ...accountInitObject, ...loadedLocalSettings } + await dispatch(refreshAccountReferral()) - for (const userInfo of context.localUsers) { - if ( - userInfo.loginId === account.rootLoginId && - userInfo.pinLoginEnabled - ) { - accountInitObject.pinLoginEnabled = true - } - } + refreshTouchId(account).catch(() => { + // We have always failed silently here + }) - const defaultDenominationSettings = state.ui.settings.denominationSettings - const syncedDenominationSettings = - syncedSettings?.denominationSettings ?? {} - const mergedDenominationSettings = {} - - for (const plugin of Object.keys(defaultDenominationSettings)) { - // @ts-expect-error - mergedDenominationSettings[plugin] = {} - // @ts-expect-error - for (const code of Object.keys(defaultDenominationSettings[plugin])) { - // @ts-expect-error - mergedDenominationSettings[plugin][code] = { - // @ts-expect-error - ...defaultDenominationSettings[plugin][code], - ...(syncedDenominationSettings?.[plugin]?.[code] ?? {}) - } + if ( + await showNotificationPermissionReminder({ + appName: config.appName, + onLogEvent(event, values) { + dispatch(logEvent(event, values)) + }, + onNotificationPermit(info) { + dispatch(updateNotificationSettings(info.notificationOptIns)).catch( + (error: unknown) => { + trackError(error, 'LoginScene:onLogin:setDeviceSettings') + console.error(error) + } + ) } - } - accountInitObject.denominationSettings = { ...mergedDenominationSettings } - - dispatch({ - type: 'ACCOUNT_INIT_COMPLETE', - data: { ...accountInitObject } }) - - await dispatch(refreshAccountReferral()) - - refreshTouchId(account).catch(() => { - // We have always failed silently here - }) - if ( - await showNotificationPermissionReminder({ - appName: config.appName, - onLogEvent(event, values) { - dispatch(logEvent(event, values)) - }, - onNotificationPermit(info) { - dispatch(updateNotificationSettings(info.notificationOptIns)).catch( - error => { - trackError(error, 'LoginScene:onLogin:setDeviceSettings') - console.error(error) - } - ) - } - }) - ) { - hideSurvey = true - } - } catch (error: any) { - showError(error) + ) { + hideSurvey = true } - // Post login stuff: + // Post login stuff: Survey modal (existing accounts only) if ( !newAccount && !hideSurvey && @@ -358,6 +201,117 @@ export function initializeAccount( } } +/** + * Navigate to wallet creation flow for new accounts. + */ +async function navigateToNewAccountFlow( + rootNavigation: NavigationBase, + account: EdgeAccount, + syncedSettings: SyncedAccountSettings, + referralPromise: Promise, + dispatch: Dispatch, + getState: GetState +): Promise { + await referralPromise + let { defaultFiat } = syncedSettings + + const [phoneCurrency] = getCurrencies() + if (typeof phoneCurrency === 'string' && phoneCurrency.length >= 3) { + defaultFiat = phoneCurrency + } + + // Ensure the creation reason is available before creating wallets: + const accountReferralCurrencyCodes = + getState().account.accountReferral.currencyCodes + const defaultSelection = + accountReferralCurrencyCodes != null + ? currencyCodesToEdgeAssets(account, accountReferralCurrencyCodes) + : config.defaultWallets + const fiatCurrencyCode = 'iso:' + defaultFiat + + // Ensure we have initialized the account settings first so we can begin + // keeping track of token warnings shown from the initial selected assets + // during account creation + await readLocalAccountSettings(account) + + const newAccountFlow = async ( + navigation: EdgeAppSceneProps< + 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' + >['navigation'], + items: WalletCreateItem[] + ): Promise => { + navigation.replace('edgeTabs', { screen: 'home' }) + const createWalletsPromise = createCustomWallets( + account, + fiatCurrencyCode, + items, + dispatch + ).catch((error: unknown) => { + showError(error) + }) + + // New user FIO handle registration flow (if env is properly configured) + const { freeRegApiToken = '', freeRegRefCode = '' } = + typeof ENV.FIO_INIT === 'object' ? ENV.FIO_INIT : {} + if (freeRegApiToken !== '' && freeRegRefCode !== '') { + const isCreateHandle = await Airship.show(bridge => ( + + )) + if (isCreateHandle) { + navigation.navigate('fioCreateHandle', { + freeRegApiToken, + freeRegRefCode + }) + } + } + + await createWalletsPromise + dispatch( + logEvent('Signup_Complete', { + numAccounts: getState().core.context.localUsers.length + }) + ) + } + + rootNavigation.replace('edgeApp', { + screen: 'edgeAppStack', + params: { + screen: 'createWalletSelectCryptoNewAccount', + params: { + newAccountFlow, + defaultSelection, + disableLegacy: true + } + } + }) +} + +/** + * Navigate to home screen for existing accounts. + */ +function navigateToExistingAccountHome( + rootNavigation: NavigationBase, + referralPromise: Promise +): void { + const { defaultScreen } = getDeviceSettings() + rootNavigation.replace('edgeApp', { + screen: 'edgeAppStack', + params: { + screen: 'edgeTabs', + params: + defaultScreen === 'home' + ? { screen: 'home' } + : { screen: 'walletsTab', params: { screen: 'walletList' } } + } + }) + referralPromise.catch(() => { + console.log(`Failed to load account referral info`) + }) +} + export function getRootNavigation(navigation: NavigationBase): NavigationBase { while (true) { const parent: NavigationBase = navigation.getParent() @@ -434,7 +388,7 @@ async function createCustomWallets( account.createCurrencyWallets(options), timeoutMs, new Error(lstrings.error_creating_wallets) - ).catch(error => { + ).catch((error: unknown) => { dispatch(logEvent('Signup_Wallets_Created_Failed', { error })) throw error }) diff --git a/src/actions/RequestReviewActions.tsx b/src/actions/RequestReviewActions.tsx index 9abe150f622..96b9bf49e1e 100644 --- a/src/actions/RequestReviewActions.tsx +++ b/src/actions/RequestReviewActions.tsx @@ -112,14 +112,15 @@ export const readReviewTriggerData = async ( const swapCountData = JSON.parse(swapCountDataStr) // Initialize new data structure with old swap count data + const swapCount = parseInt(swapCountData.swapCount) const migratedData: ReviewTriggerData = { ...initReviewTriggerData(), - swapCount: parseInt(swapCountData.swapCount) || 0 + swapCount: Number.isNaN(swapCount) ? 0 : swapCount } // If user was already asked for review in the old system, // set nextTriggerDate to 1 year in the future - if (swapCountData.hasReviewBeenRequested) { + if (swapCountData.hasReviewBeenRequested === true) { const nextYear = new Date() nextYear.setFullYear(nextYear.getFullYear() + 1) migratedData.nextTriggerDate = nextYear diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 21f4ae9425c..588abcf9d41 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -14,7 +14,6 @@ import type { EdgeDenomination, EdgeSwapPluginType } from 'edge-core-js' -import { disableTouchId, enableTouchId } from 'edge-login-ui-rn' import * as React from 'react' import { ButtonsModal } from '../components/modals/ButtonsModal' @@ -225,59 +224,6 @@ export function setDenominationKeyRequest( } } -// touch id interaction -export function updateTouchIdEnabled( - isTouchEnabled: boolean, - account: EdgeAccount -): ThunkAction> { - return async (dispatch, getState) => { - // dispatch the update for the new state for - dispatch({ - type: 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS', - data: { isTouchEnabled } - }) - if (isTouchEnabled) { - await enableTouchId(account) - } else { - await disableTouchId(account) - } - } -} - -export function togglePinLoginEnabled( - pinLoginEnabled: boolean -): ThunkAction> { - return async (dispatch, getState) => { - const state = getState() - const { context, account } = state.core - - dispatch({ - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED', - data: { pinLoginEnabled } - }) - return await account - .changePin({ enableLogin: pinLoginEnabled }) - .catch(async (error: unknown) => { - showError(error) - - let pinLoginEnabled = false - for (const userInfo of context.localUsers) { - if ( - userInfo.loginId === account.rootLoginId && - userInfo.pinLoginEnabled - ) { - pinLoginEnabled = true - } - } - - dispatch({ - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED', - data: { pinLoginEnabled } - }) - }) - } -} - export async function showReEnableOtpModal( account: EdgeAccount ): Promise { @@ -438,6 +384,8 @@ export const asSyncedAccountSettings = asObject({ asDenominationSettings, () => ({}) ), + // Flag to track one-time denomination settings cleanup migration + denominationSettingsOptimized: asMaybe(asBoolean, false), securityCheckedWallets: asMaybe( asSecurityCheckedWallets, () => ({}) @@ -565,10 +513,9 @@ export async function readSyncedSettings( const text = await account.disklet.getText(SYNCED_SETTINGS_FILENAME) const settingsFromFile = JSON.parse(text) return asSyncedAccountSettings(settingsFromFile) - } catch (e: any) { - console.log(e) - // If Settings.json doesn't exist yet, create it, and return it - await writeSyncedSettings(account, SYNCED_ACCOUNT_DEFAULTS) + } catch (error: unknown) { + // If Settings.json doesn't exist yet, return defaults without writing. + // Defaults can be derived from cleaners. Only write when values change. return SYNCED_ACCOUNT_DEFAULTS } } @@ -596,3 +543,95 @@ const updateCurrencySettings = ( updatedSettings.denominationSettings[pluginId][currencyCode] = denomination return updatedSettings } + +/** + * One-time migration to clean up denomination settings by removing entries + * that match the default values from currencyInfo. This reduces the size of + * the synced settings file and speeds up subsequent logins. + * + * Only runs once per account - tracked via denominationSettingsOptimized flag. + */ +export async function migrateDenominationSettings( + account: EdgeAccount, + syncedSettings: SyncedAccountSettings +): Promise { + const { denominationSettings, denominationSettingsOptimized } = syncedSettings + + // Already migrated or no settings to clean + if (denominationSettingsOptimized) return + if ( + denominationSettings == null || + Object.keys(denominationSettings).length === 0 + ) { + // No denomination settings to clean, just set the flag + await writeSyncedSettings(account, { + ...syncedSettings, + denominationSettingsOptimized: true + }) + return + } + + // Clean up denomination settings by removing entries that match defaults + const cleanedSettings: DenominationSettings = {} + let needsCleanup = false + + for (const pluginId of Object.keys(denominationSettings)) { + const currencyConfig = account.currencyConfig[pluginId] + if (currencyConfig == null) continue + + const { currencyInfo, allTokens } = currencyConfig + const pluginDenoms = denominationSettings[pluginId] + if (pluginDenoms == null) continue + + cleanedSettings[pluginId] = {} + + for (const currencyCode of Object.keys(pluginDenoms)) { + const savedDenom = pluginDenoms[currencyCode] + if (savedDenom == null) continue + + // Find the default denomination for this currency + let defaultDenom: EdgeDenomination | undefined + if (currencyCode === currencyInfo.currencyCode) { + defaultDenom = currencyInfo.denominations[0] + } else { + // Look for token + for (const tokenId of Object.keys(allTokens)) { + const token = allTokens[tokenId] + if (token.currencyCode === currencyCode) { + defaultDenom = token.denominations[0] + break + } + } + } + + // Only keep if different from default + if ( + defaultDenom == null || + savedDenom.multiplier !== defaultDenom.multiplier || + savedDenom.name !== defaultDenom.name + ) { + // @ts-expect-error - DenominationSettings type allows undefined + cleanedSettings[pluginId][currencyCode] = savedDenom + } else { + needsCleanup = true + } + } + + // Remove empty plugin entries + if (Object.keys(cleanedSettings[pluginId] ?? {}).length === 0) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cleanedSettings[pluginId] + } + } + + // Write cleaned settings with optimization flag + await writeSyncedSettings(account, { + ...syncedSettings, + denominationSettings: cleanedSettings, + denominationSettingsOptimized: true + }) + + if (needsCleanup) { + console.log('Denomination settings cleaned up - removed default values') + } +} diff --git a/src/actions/WalletListMenuActions.tsx b/src/actions/WalletListMenuActions.tsx index a705c294847..5dd611b7315 100644 --- a/src/actions/WalletListMenuActions.tsx +++ b/src/actions/WalletListMenuActions.tsx @@ -27,7 +27,6 @@ import { logActivity } from '../util/logger' import { validatePassword } from './AccountActions' import { showDeleteWalletModal } from './DeleteWalletModalActions' import { showResyncWalletModal } from './ResyncWalletModalActions' -import { showScamWarningModal } from './ScamWarningActions' import { toggleUserPausedWallet } from './SettingsActions' export type WalletListMenuKey = @@ -208,9 +207,6 @@ export function walletListMenuAction( const wallet = account.currencyWallets[walletId] const { xpubExplorer } = wallet.currencyInfo - // Show the scam warning modal if needed - await showScamWarningModal('firstPrivateKeyView') - const displayPublicSeed = await account.getDisplayPublicKey(wallet.id) const copy: ButtonInfo = { @@ -283,9 +279,6 @@ export function walletListMenuAction( const { currencyWallets } = account const wallet = currencyWallets[walletId] - // Show the scam warning modal if needed - await showScamWarningModal('firstPrivateKeyView') - const passwordValid = (await dispatch( validatePassword({ diff --git a/src/components/scenes/ReviewTriggerTestScene.tsx b/src/components/scenes/ReviewTriggerTestScene.tsx index 00f2f50a322..65fe90b480f 100644 --- a/src/components/scenes/ReviewTriggerTestScene.tsx +++ b/src/components/scenes/ReviewTriggerTestScene.tsx @@ -37,7 +37,7 @@ import { EdgeText } from '../themed/EdgeText' interface Props extends EdgeSceneProps<'reviewTriggerTest'> {} -export const ReviewTriggerTestScene = (props: Props) => { +export const ReviewTriggerTestScene: React.FC = () => { const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index d5c9339d762..9c597e6f861 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -1,5 +1,11 @@ +import { useQuery } from '@tanstack/react-query' import type { EdgeLogType } from 'edge-core-js' -import { getSupportedBiometryType } from 'edge-login-ui-rn' +import { + disableTouchId, + enableTouchId, + getSupportedBiometryType, + isTouchEnabled +} from 'edge-login-ui-rn' import * as React from 'react' import { Platform } from 'react-native' import { check } from 'react-native-permissions' @@ -22,9 +28,7 @@ import { logoutRequest } from '../../actions/LoginActions' import { setAutoLogoutTimeInSecondsRequest, showReEnableOtpModal, - showUnlockSettingsModal, - togglePinLoginEnabled, - updateTouchIdEnabled + showUnlockSettingsModal } from '../../actions/SettingsActions' import { ENV } from '../../env' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -57,7 +61,7 @@ import { SettingsTappableRow } from '../settings/SettingsTappableRow' type Props = EdgeAppSceneProps<'settingsOverview'> -export const SettingsScene = (props: Props) => { +export const SettingsScene: React.FC = props => { const { navigation } = props const theme = useTheme() const dispatch = useDispatch() @@ -70,16 +74,40 @@ export const SettingsScene = (props: Props) => { state => state.ui.settings.developerModeOn ) const isLocked = useSelector(state => state.ui.settings.changesLocked) - const pinLoginEnabled = useSelector( - state => state.ui.settings.pinLoginEnabled - ) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) - const supportsTouchId = useSelector( - state => state.ui.settings.isTouchSupported - ) - const touchIdEnabled = useSelector(state => state.ui.settings.isTouchEnabled) const account = useSelector(state => state.core.account) + + // Load biometric state locally (not from Redux) + const { data: biometricState } = useQuery({ + queryKey: ['biometricState', account.id], + queryFn: async () => { + const [touchEnabled, supportedType] = await Promise.all([ + isTouchEnabled(account), + getSupportedBiometryType() + ]) + return { + isTouchEnabled: touchEnabled, + isTouchSupported: supportedType !== false, + biometryType: supportedType + } + }, + enabled: account != null + }) + + // Local state to track touch ID enabled status (can be toggled by user) + const [touchIdEnabled, setTouchIdEnabled] = React.useState( + null + ) + + // Sync local state with loaded state + React.useEffect(() => { + if (biometricState != null && touchIdEnabled == null) { + setTouchIdEnabled(biometricState.isTouchEnabled) + } + }, [biometricState, touchIdEnabled]) + + const supportsTouchId = biometricState?.isTouchSupported ?? false const username = useWatch(account, 'username') const allKeys = useWatch(account, 'allKeys') const hasRestoreWallets = @@ -89,6 +117,15 @@ export const SettingsScene = (props: Props) => { const context = useSelector(state => state.core.context) const logSettings = useWatch(context, 'logSettings') + // Load pin login state locally (not from Redux) and make it mutable + const [pinLoginEnabled, setPinLoginEnabled] = React.useState( + () => + context?.localUsers?.some( + userInfo => + userInfo.loginId === account.rootLoginId && userInfo.pinLoginEnabled + ) ?? false + ) + const [localContactPermissionOn, setLocalContactsPermissionOn] = React.useState(false) const [isDarkTheme, setIsDarkTheme] = React.useState( @@ -135,10 +172,12 @@ export const SettingsScene = (props: Props) => { }) setValidatedPassword(undefined) } else { - const password = await handleShowUnlockSettingsModal().catch(err => { - showError(err) - return undefined - }) + const password = await handleShowUnlockSettingsModal().catch( + (error: unknown) => { + showError(error) + return undefined + } + ) setValidatedPassword(password) } }) @@ -146,10 +185,12 @@ export const SettingsScene = (props: Props) => { /** Returns true if the settings are locked. Otherwise false if they're unlocked. */ const hasLock = async (): Promise => { if (isLocked) { - const password = await handleShowUnlockSettingsModal().catch(err => { - showError(err) - return undefined - }) + const password = await handleShowUnlockSettingsModal().catch( + (error: unknown) => { + showError(error) + return undefined + } + ) if (password == null) return true setValidatedPassword(password) dispatch({ @@ -161,7 +202,20 @@ export const SettingsScene = (props: Props) => { } const handleUpdateTouchId = useHandler(async () => { - await dispatch(updateTouchIdEnabled(!touchIdEnabled, account)) + if (touchIdEnabled == null) return + const newValue = !touchIdEnabled + setTouchIdEnabled(newValue) + try { + if (newValue) { + await enableTouchId(account) + } else { + await disableTouchId(account) + } + } catch (error: unknown) { + // Revert on error + setTouchIdEnabled(!newValue) + showError(error) + } }) const handleClearLogs = useHandler(async () => { @@ -173,7 +227,15 @@ export const SettingsScene = (props: Props) => { }) const handleTogglePinLoginEnabled = useHandler(async () => { - await dispatch(togglePinLoginEnabled(!pinLoginEnabled)) + const newValue = !pinLoginEnabled + setPinLoginEnabled(newValue) + try { + await account.changePin({ enableLogin: newValue }) + } catch (error: unknown) { + // Revert on error + setPinLoginEnabled(!newValue) + showError(error) + } }) const handleToggleDarkTheme = useHandler(async () => { @@ -289,8 +351,8 @@ export const SettingsScene = (props: Props) => { bridge={bridge} message={sprintf(lstrings.delete_account_feedback, username)} /> - )).catch(err => { - showDevError(err) + )).catch((error: unknown) => { + showDevError(error) }) return true }} @@ -341,7 +403,7 @@ export const SettingsScene = (props: Props) => { defaultLogLevel: newDefaultLogLevel, sources: {} }) - .catch(error => { + .catch((error: unknown) => { showError(error) }) }) @@ -355,58 +417,51 @@ export const SettingsScene = (props: Props) => { await writeForceLightAccountCreate(!forceLightAccountCreate) }) - const loadBiometryType = async () => { + useAsyncEffect( + async () => { + const currentContactsPermission = await check(permissionNames.contacts) + setLocalContactsPermissionOn(currentContactsPermission === 'granted') + }, + [], + 'SettingsScene' + ) + + // Update biometry text based on loaded biometry type + React.useEffect(() => { + if (biometricState == null) return + if (Platform.OS === 'ios') { - const biometryType = await getSupportedBiometryType() - switch (biometryType) { + switch (biometricState.biometryType) { case 'FaceID': setTouchIdText(lstrings.settings_button_use_faceID) break case 'TouchID': setTouchIdText(lstrings.settings_button_use_touchID) break - case false: break } } else { setTouchIdText(lstrings.settings_button_use_biometric) } - } + }, [biometricState]) - useAsyncEffect( - async () => { - const currentContactsPermission = await check(permissionNames.contacts) - setLocalContactsPermissionOn(currentContactsPermission === 'granted') - }, - [], - 'SettingsScene' - ) - - // Load biometry type on mount + // Watch for logSettings changes React.useEffect(() => { - if (!supportsTouchId) return - - loadBiometryType().catch(error => { - showError(error) - }) - - // Watch for logSettings changes const cleanup = context.watch('logSettings', logSettings => { setDefaultLogLevel(logSettings.defaultLogLevel) }) - // Cleanup function to remove the watcher on unmount return () => { - if (cleanup) cleanup() + if (cleanup != null) cleanup() } - }, [context, supportsTouchId]) + }, [context]) // Show a modal if we have a pending OTP resent when we enter the scene: React.useEffect(() => { return navigation.addListener('focus', () => { if (account.otpResetDate != null) { - showReEnableOtpModal(account).catch(error => { + showReEnableOtpModal(account).catch((error: unknown) => { showError(error) }) } @@ -512,22 +567,22 @@ export const SettingsScene = (props: Props) => { onPress={handleDefaultFiat} /> - {isLightAccount ? null : ( + {!isLightAccount ? ( - )} - {supportsTouchId && !isLightAccount && ( + ) : null} + {supportsTouchId && !isLightAccount && touchIdEnabled != null ? ( - )} + ) : null} {} -export function WalletListScene(props: Props) { +export const WalletListScene: React.FC = props => { const { navigation } = props const theme = useTheme() const styles = getStyles(theme) @@ -77,7 +77,7 @@ export function WalletListScene(props: Props) { setSorting(true) } }) - .catch(error => { + .catch((error: unknown) => { showError(error) }) }) diff --git a/src/components/scenes/WcConnectionsScene.tsx b/src/components/scenes/WcConnectionsScene.tsx index bccad41f3aa..b4178d4762f 100644 --- a/src/components/scenes/WcConnectionsScene.tsx +++ b/src/components/scenes/WcConnectionsScene.tsx @@ -8,7 +8,6 @@ import AntDesignIcon from 'react-native-vector-icons/AntDesign' import { sprintf } from 'sprintf-js' import { checkAndShowLightBackupModal } from '../../actions/BackupModalActions' -import { showScamWarningModal } from '../../actions/ScamWarningActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -125,9 +124,6 @@ export const WcConnectionsScene = (props: Props) => { } const handleNewConnectionPress = async () => { - // Show the scam warning modal if needed - await showScamWarningModal('firstWalletConnect') - if (checkAndShowLightBackupModal(account, navigation as NavigationBase)) { await Promise.resolve() } else { diff --git a/src/reducers/PasswordReminderReducer.ts b/src/reducers/PasswordReminderReducer.ts index 30e82a43c6c..88b592269f1 100644 --- a/src/reducers/PasswordReminderReducer.ts +++ b/src/reducers/PasswordReminderReducer.ts @@ -200,30 +200,22 @@ export const untranslatedReducer: Reducer< } function translateAction(action: Action): PasswordReminderReducerAction { - if ( - action.type === 'ACCOUNT_INIT_COMPLETE' && - action.data.account.newAccount - ) { + if (action.type === 'LOGIN' && action.data.account.newAccount) { const now = Date.now() return { type: 'NEW_ACCOUNT_LOGIN', data: { - lastLoginDate: now, - // @ts-expect-error - lastPasswordUseDate: now + lastLoginDate: now } } } - if ( - action.type === 'ACCOUNT_INIT_COMPLETE' && - action.data.account.passwordLogin - ) { + if (action.type === 'LOGIN' && action.data.account.passwordLogin) { const now = Date.now() return { type: 'PASSWORD_LOGIN', data: { - ...action.data.passwordReminder, + ...action.data.localSettings.passwordReminder, lastLoginDate: now, lastPasswordUseDate: now } @@ -231,7 +223,7 @@ function translateAction(action: Action): PasswordReminderReducerAction { } if ( - action.type === 'ACCOUNT_INIT_COMPLETE' && + action.type === 'LOGIN' && !action.data.account.passwordLogin && !action.data.account.newAccount && action.data.account.username != null @@ -239,7 +231,7 @@ function translateAction(action: Action): PasswordReminderReducerAction { return { type: 'NON_PASSWORD_LOGIN', data: { - ...action.data.passwordReminder, + ...action.data.localSettings.passwordReminder, lastLoginDate: Date.now() } } diff --git a/src/reducers/SpendingLimitsReducer.ts b/src/reducers/SpendingLimitsReducer.ts index d65fff7ef12..af413565685 100644 --- a/src/reducers/SpendingLimitsReducer.ts +++ b/src/reducers/SpendingLimitsReducer.ts @@ -13,9 +13,11 @@ export const initialState: SpendingLimits = { export const isEnabled = ( state: boolean = initialState.transaction.isEnabled, action: Action -) => { +): boolean => { switch (action.type) { - case 'ACCOUNT_INIT_COMPLETE': + case 'LOGIN': { + return action.data.localSettings.spendingLimits.transaction.isEnabled + } case 'SPENDING_LIMITS/NEW_SPENDING_LIMITS': { return action.data.spendingLimits.transaction.isEnabled } @@ -27,9 +29,11 @@ export const isEnabled = ( export const amount = ( state: number = initialState.transaction.amount, action: Action -) => { +): number => { switch (action.type) { - case 'ACCOUNT_INIT_COMPLETE': + case 'LOGIN': { + return action.data.localSettings.spendingLimits.transaction.amount + } case 'SPENDING_LIMITS/NEW_SPENDING_LIMITS': { return action.data.spendingLimits.transaction.amount } diff --git a/src/reducers/scenes/SettingsReducer.ts b/src/reducers/scenes/SettingsReducer.ts index 3df018553ae..f75ea3af39d 100644 --- a/src/reducers/scenes/SettingsReducer.ts +++ b/src/reducers/scenes/SettingsReducer.ts @@ -1,10 +1,10 @@ -import type { EdgeAccount, EdgeTokenId } from 'edge-core-js' +import type { EdgeAccount } from 'edge-core-js' import { asSyncedAccountSettings, + type DenominationSettings, type SyncedAccountSettings } from '../../actions/SettingsActions' -import type { SortOption } from '../../components/modals/WalletListSortModal' import type { Action } from '../../types/reduxTypes' import { asLocalAccountSettings, @@ -16,9 +16,6 @@ export const initialState: SettingsState = { ...asSyncedAccountSettings({}), ...asLocalAccountSettings({}), changesLocked: true, - isTouchEnabled: false, - isTouchSupported: false, - pinLoginEnabled: false, settingsLoaded: null, userPausedWalletsSet: null } @@ -27,9 +24,6 @@ export interface SettingsState extends LocalAccountSettings, SyncedAccountSettings { changesLocked: boolean - isTouchEnabled: boolean - isTouchSupported: boolean - pinLoginEnabled: boolean settingsLoaded: boolean | null // A copy of `userPausedWallets`, but as a set. @@ -37,14 +31,10 @@ export interface SettingsState userPausedWalletsSet: Set | null } -export interface AccountInitPayload extends SettingsState { +export interface LoginPayload { account: EdgeAccount - tokenId: EdgeTokenId - pinLoginEnabled: boolean - isTouchEnabled: boolean - isTouchSupported: boolean - walletId: string - walletsSort: SortOption + syncedSettings: SyncedAccountSettings + localSettings: LocalAccountSettings } export const settingsLegacy = ( @@ -53,54 +43,32 @@ export const settingsLegacy = ( ): SettingsState => { switch (action.type) { case 'LOGIN': { - const { account, walletSort } = action.data - - // Setup default denominations for settings based on currencyInfo - const newState = { ...state, walletSort } - for (const pluginId of Object.keys(account.currencyConfig)) { - const { currencyInfo } = account.currencyConfig[pluginId] - const { currencyCode } = currencyInfo - if (newState.denominationSettings[pluginId] == null) - state.denominationSettings[pluginId] = {} - // @ts-expect-error - this is because laziness - newState.denominationSettings[pluginId][currencyCode] ??= - currencyInfo.denominations[0] - for (const token of currencyInfo.metaTokens ?? []) { - const tokenCode = token.currencyCode - // @ts-expect-error - this is because laziness - newState.denominationSettings[pluginId][tokenCode] = - token.denominations[0] - } - } - return newState - } - - case 'ACCOUNT_INIT_COMPLETE': { + const { syncedSettings, localSettings } = action.data const { autoLogoutTimeInSeconds, - contactsPermissionShown, countryCode, defaultFiat, defaultIsoFiat, denominationSettings, - developerModeOn, - isAccountBalanceVisible, - isTouchEnabled, - isTouchSupported, mostRecentWallets, passwordRecoveryRemindersShown, - userPausedWallets, - pinLoginEnabled, preferredSwapPluginId, preferredSwapPluginType, securityCheckedWallets, - spamFilterOn, stateProvinceCode, + userPausedWallets, walletsSort, rampLastFiatCurrencyCode, rampLastCryptoSelection - } = action.data - const newState: SettingsState = { + } = syncedSettings + const { + contactsPermissionShown, + developerModeOn, + isAccountBalanceVisible, + spamFilterOn + } = localSettings + + return { ...state, autoLogoutTimeInSeconds, contactsPermissionShown, @@ -110,13 +78,10 @@ export const settingsLegacy = ( denominationSettings, developerModeOn, isAccountBalanceVisible, - isTouchEnabled, - isTouchSupported, mostRecentWallets, passwordRecoveryRemindersShown, userPausedWallets, userPausedWalletsSet: new Set(userPausedWallets), - pinLoginEnabled, preferredSwapPluginId: preferredSwapPluginId === '' ? undefined : preferredSwapPluginId, preferredSwapPluginType, @@ -128,7 +93,6 @@ export const settingsLegacy = ( rampLastFiatCurrencyCode, rampLastCryptoSelection } - return newState } case 'DEVELOPER_MODE_ON': { return { ...state, developerModeOn: true } @@ -143,23 +107,24 @@ export const settingsLegacy = ( return { ...state, spamFilterOn: false } } - case 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED': { - const { pinLoginEnabled } = action.data - return { - ...state, - pinLoginEnabled - } - } - case 'UI/SETTINGS/SET_DENOMINATION_KEY': { const { pluginId, currencyCode, denomination } = action.data - const newDenominationSettings = { ...state.denominationSettings } - // @ts-expect-error - this is because laziness - newDenominationSettings[pluginId][currencyCode] = denomination + + // Ensure pluginId object exists before setting denomination + const newDenominationSettings: DenominationSettings = { + ...state.denominationSettings, + [pluginId]: { + ...state.denominationSettings[pluginId], + [currencyCode]: { + ...denomination, + symbol: denomination.symbol ?? undefined + } + } + } return { ...state, - ...newDenominationSettings + denominationSettings: newDenominationSettings } } @@ -210,13 +175,6 @@ export const settingsLegacy = ( } } - case 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS': { - return { - ...state, - isTouchEnabled: action.data.isTouchEnabled - } - } - case 'UI/SETTINGS/SET_MOST_RECENT_WALLETS': { return { ...state, diff --git a/src/types/reduxActions.ts b/src/types/reduxActions.ts index d96b778d82b..724383183b0 100644 --- a/src/types/reduxActions.ts +++ b/src/types/reduxActions.ts @@ -1,6 +1,5 @@ import type { Disklet } from 'disklet' import type { - EdgeAccount, EdgeContext, EdgeCurrencyWallet, EdgeDenomination, @@ -20,7 +19,7 @@ import type { LoanManagerActions } from '../controllers/loan-manager/redux/actio import type { CcWalletMap } from '../reducers/FioReducer' import type { PermissionsState } from '../reducers/PermissionsReducer' import type { - AccountInitPayload, + LoginPayload, SettingsState } from '../reducers/scenes/SettingsReducer' import type { StakingAction } from '../reducers/StakingReducer' @@ -58,7 +57,6 @@ type NoDataActionName = export type Action = | { type: NoDataActionName } // Actions with known payloads: - | { type: 'ACCOUNT_INIT_COMPLETE'; data: AccountInitPayload } | { type: 'ACCOUNT_REFERRAL_LOADED' data: { referral: AccountReferral; cache: ReferralCache } @@ -88,7 +86,7 @@ export type Action = type: 'IS_NOTIFICATION_VIEW_ACTIVE' data: { isNotificationViewActive: boolean } } - | { type: 'LOGIN'; data: { account: EdgeAccount; walletSort: SortOption } } + | { type: 'LOGIN'; data: LoginPayload } | { type: 'MESSAGE_TWEAK_HIDDEN' data: { messageId: string; source: TweakSource } @@ -109,10 +107,6 @@ export type Action = type: 'UPDATE_FIO_WALLETS' data: { fioWallets: EdgeCurrencyWallet[] } } - | { - type: 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS' - data: { isTouchEnabled: boolean } - } | { type: 'UI/SETTINGS/SET_ACCOUNT_BALANCE_VISIBILITY' data: { isAccountBalanceVisible: boolean } @@ -153,10 +147,6 @@ export type Action = data: { userPausedWallets: string[] } } | { type: 'UI/SETTINGS/SET_WALLETS_SORT'; data: { walletsSort: SortOption } } - | { - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED' - data: { pinLoginEnabled: boolean } - } | { type: 'UI/SETTINGS/UPDATE_SETTINGS'; data: { settings: SettingsState } } | { type: 'UI/SET_COUNTRY_CODE'; data: { countryCode: string | undefined } } | { diff --git a/src/util/fake/FakeProviders.tsx b/src/util/fake/FakeProviders.tsx index ed15493363c..acd24a3850d 100644 --- a/src/util/fake/FakeProviders.tsx +++ b/src/util/fake/FakeProviders.tsx @@ -1,4 +1,5 @@ import { NavigationContext } from '@react-navigation/native' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import * as React from 'react' import { type Metrics, SafeAreaProvider } from 'react-native-safe-area-context' import { Provider } from 'react-redux' @@ -9,6 +10,14 @@ import { rootReducer, type RootState } from '../../reducers/RootReducer' import { renderStateProviders } from '../../state/renderStateProviders' import { fakeNavigation } from './fakeSceneProps' +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false + } + } +}) + type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial @@ -22,7 +31,7 @@ interface Props { initialState?: FakeState } -export function FakeProviders(props: Props) { +export function FakeProviders(props: Props): React.JSX.Element { const { children, initialState = {} } = props const store = React.useMemo( @@ -30,13 +39,15 @@ export function FakeProviders(props: Props) { [initialState] ) return ( - - {renderStateProviders( - - {children} - - )} - + + + {renderStateProviders( + + {children} + + )} + + ) }