Skip to content
Merged

preview #4467

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mobile/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ function AppShell({children}: React.PropsWithChildren) {
const isMigrated = useMigrations(rootStorage)
const isLoaded = useFonts()

// Wait for migrations and fonts to load
// useMigrations handles errors internally by clearing storage if needed
if (!isMigrated || !isLoaded) return null

return (
Expand Down
2 changes: 1 addition & 1 deletion mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Yoroi",
"slug": "yoroi",
"owner": "emurgo",
"version": "7.0.2",
"version": "7.0.3",
"orientation": "portrait",
"icon": "./assets/yoroi/icon.png",
"userInterfaceStyle": "automatic",
Expand Down
4 changes: 2 additions & 2 deletions mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "yoroi-v2",
"version": "7.0.2",
"version": "7.0.3",
"main": "index.ts",
"scripts": {
"postinstall": "patch-package",
Expand Down
5 changes: 5 additions & 0 deletions mobile/packages/types/links/cardano-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import {
DRepId,
TransactionHash,
} from '../branded'
import {ChainSupportedNetworks} from '../chain/network'

export type CardanoActionSendOnlyReceiver = Readonly<{
action: 'send-only-receiver'
receiver: Address
network?: ChainSupportedNetworks
}>

export type CardanoActionSendSinglePt = Readonly<{
action: 'send-single-pt'
receiver: Address
network?: ChainSupportedNetworks
params:
| {
amount: number | undefined
Expand Down Expand Up @@ -58,6 +61,7 @@ export type CardanoActionBrowseDapp = Readonly<{
export type CardanoActionPayRequest = Readonly<{
action: 'pay-request'
address: Address
network?: ChainSupportedNetworks
amount?: BalanceQuantity
asset?: string
memo?: string
Expand Down Expand Up @@ -87,6 +91,7 @@ export type CardanoActionViewBlock = Readonly<{
export type CardanoActionViewAddress = Readonly<{
action: 'view-address'
address: Address
network?: ChainSupportedNetworks
}>

export type CardanoActionP2PConnect = Readonly<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,84 @@ import * as React from 'react'

import {WalletManager} from '../wallet-manager'

const HYDRATION_TIMEOUT_MS = 30_000 // 30 seconds max for wallet hydration

/**
* Wrapper component that ensures wallet metadata is hydrated before rendering children.
* This prevents race conditions where navigation checks for wallets before they're loaded from storage.
* Includes timeout protection to prevent infinite hangs.
*/
export const WalletManagerHydrationWrapper: React.FC<
React.PropsWithChildren<{
walletManager: WalletManager
/** Optional timeout in milliseconds (default: 30000) */
timeoutMs?: number
}>
> = ({children, walletManager}) => {
> = ({children, walletManager, timeoutMs = HYDRATION_TIMEOUT_MS}) => {
const [isHydrated, setIsHydrated] = React.useState(false)
const [hydrationError, setHydrationError] = React.useState<Error | null>(null)

React.useEffect(() => {
let isMounted = true
let timeoutId: ReturnType<typeof setTimeout> | null = null

// Set up timeout
const timeoutPromise = new Promise<'timeout'>((resolve) => {
timeoutId = setTimeout(() => resolve('timeout'), timeoutMs)
})

// Race between hydration and timeout
Promise.race([walletManager.hydrate(), timeoutPromise])
.then((result) => {
if (!isMounted) return

walletManager
.hydrate()
.then(() => {
if (isMounted) {
if (result === 'timeout') {
getLogger().error(
'WalletManagerHydrationWrapper: hydration timed out',
{
timeoutMs,
},
)
setHydrationError(
new Error(`Wallet hydration timed out after ${timeoutMs / 1000}s`),
)
// Still allow app to continue - wallets may load later
setIsHydrated(true)
return
}

setIsHydrated(true)
})
.catch((error) => {
if (!isMounted) return

getLogger().error('WalletManagerHydrationWrapper: hydration failed', {
error,
})
setHydrationError(
error instanceof Error ? error : new Error(String(error)),
)
// Still render children even if hydration fails to prevent app from being stuck
if (isMounted) {
setIsHydrated(true)
}
setIsHydrated(true)
})

return () => {
isMounted = false
if (timeoutId) clearTimeout(timeoutId)
}
}, [walletManager, timeoutMs])

// Log hydration error for debugging but don't block UI
React.useEffect(() => {
if (hydrationError) {
getLogger().warn(
'WalletManagerHydrationWrapper: App continuing despite hydration error',
{
error: hydrationError.message,
},
)
}
}, [walletManager])
}, [hydrationError])

if (!isHydrated) {
return null
Expand Down
3 changes: 3 additions & 0 deletions mobile/packages/wallet-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export * from './network-manager/get-wallet-factory'
// Lifecycle
export * from './lifecycle/wallet-lifecycle'

// Recovery
export {recoverOrphanedWallets} from './recovery/recover-orphaned-wallets'

// Common
export * from './common/constants'
export * from './common/validators/wallet-meta'
Expand Down
Loading
Loading