LoadingOverlay
A full-screen modal overlay with a loading indicator and optional title.Props
Optional text to display below the spinner
Top padding offset for the overlay
Additional styles for the container
Usage
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import { useState } from 'react';
function MyComponent() {
const [isLoading, setIsLoading] = useState(false);
const handleAction = async () => {
setIsLoading(true);
try {
await performAsyncOperation();
} finally {
setIsLoading(false);
}
};
return (
<>
<MyContent onAction={handleAction} />
{isLoading && <LoadingOverlay title="Processing..." />}
</>
);
}
With Animation
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
function AnimatedLoading({ visible, title }) {
if (!visible) return null;
return (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<LoadingOverlay title={title} />
</Animated.View>
);
}
Platform-Specific Behavior
The LoadingOverlay renders differently on iOS and Android:- iOS: Uses BlurView for a frosted glass effect
- Android: Uses solid background color with spinner
// iOS: BlurView with ActivityIndicator
// Android: Solid background with Spinner
<LoadingOverlay title="Loading wallet..." />
Implementation Details
The LoadingOverlay component:import React from 'react';
import { StyleSheet } from 'react-native';
import { BlurView } from 'react-native-blur-view';
import { Box, Text } from '@/design-system';
import ActivityIndicator from '@/components/ActivityIndicator';
import Spinner from '@/components/Spinner';
import { IS_ANDROID, IS_IOS } from '@/env';
type LoadingOverlayProps = {
title?: string;
paddingTop?: number;
style?: StyleProp<ViewStyle>;
};
const LoadingOverlay = ({ title, paddingTop = 0, style }: LoadingOverlayProps) => {
return (
<Box
alignItems="center"
justifyContent="center"
style={[styles.container, { paddingTop }, style]}
>
{/* Backdrop */}
<Animated.View
entering={FadeIn}
exiting={FadeOut}
style={[StyleSheet.absoluteFillObject, styles.backdrop]}
/>
{/* Loading box */}
<Box
alignItems="center"
borderRadius={20}
justifyContent="center"
overflow="hidden"
padding="20px"
style={styles.loadingBox}
>
<Box alignItems="center" flexDirection="row" justifyContent="center" zIndex={2}>
{IS_ANDROID ? <Spinner color={colors.blueGreyDark} /> : <ActivityIndicator />}
{title ? (
<Text color="labelPrimary" size="20pt" weight="semibold" style={styles.title}>
{title}
</Text>
) : null}
</Box>
{IS_IOS ? (
<BlurView
blurIntensity={40}
blurStyle={isDarkMode ? 'dark' : 'light'}
style={styles.blur}
/>
) : null}
</Box>
</Box>
);
};
Common Patterns
Async Operation with Loading
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import { useState } from 'react';
import * as p from '@/screens/Portal';
function useAsyncOperation() {
const [isLoading, setIsLoading] = useState(false);
const execute = async (operation: () => Promise<void>, loadingText: string) => {
setIsLoading(true);
try {
await operation();
} catch (error) {
console.error('Operation failed:', error);
// Handle error
} finally {
setIsLoading(false);
}
};
const LoadingComponent = () => (
isLoading ? <LoadingOverlay title="Processing..." /> : null
);
return { execute, LoadingComponent };
}
// Usage
function MyComponent() {
const { execute, LoadingComponent } = useAsyncOperation();
const handleSend = () => {
execute(
async () => {
await sendTransaction();
},
'Sending transaction...'
);
};
return (
<>
<MyContent onSend={handleSend} />
<LoadingComponent />
</>
);
}
Multi-Step Loading
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import { useState } from 'react';
function MultiStepProcess() {
const [loadingStep, setLoadingStep] = useState<string | null>(null);
const handleProcess = async () => {
try {
setLoadingStep('Preparing transaction...');
await prepareTransaction();
setLoadingStep('Signing...');
await signTransaction();
setLoadingStep('Broadcasting...');
await broadcastTransaction();
setLoadingStep('Confirming...');
await waitForConfirmation();
setLoadingStep(null);
} catch (error) {
setLoadingStep(null);
// Handle error
}
};
return (
<>
<MyContent onProcess={handleProcess} />
{loadingStep && <LoadingOverlay title={loadingStep} />}
</>
);
}
Loading with Timeout
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import { useState, useEffect } from 'react';
function useLoadingWithTimeout(timeoutMs: number = 30000) {
const [isLoading, setIsLoading] = useState(false);
const [timedOut, setTimedOut] = useState(false);
useEffect(() => {
if (!isLoading) {
setTimedOut(false);
return;
}
const timer = setTimeout(() => {
setTimedOut(true);
setIsLoading(false);
}, timeoutMs);
return () => clearTimeout(timer);
}, [isLoading, timeoutMs]);
return { isLoading, setIsLoading, timedOut };
}
// Usage
function MyComponent() {
const { isLoading, setIsLoading, timedOut } = useLoadingWithTimeout(30000);
const handleAction = async () => {
setIsLoading(true);
try {
await longRunningOperation();
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (timedOut) {
showError('Operation timed out');
}
}, [timedOut]);
return (
<>
<MyContent onAction={handleAction} />
{isLoading && <LoadingOverlay title="Loading..." />}
</>
);
}
Modal Confirmation Dialogs
While there’s no dedicated modal dialog component, you can combine Portal sheets with LoadingOverlay:Confirmation Modal
import * as p from '@/screens/Portal';
import { Box, Stack, Text } from '@/design-system';
import { SheetActionButtonRow } from '@/components/sheet/sheet-action-buttons/SheetActionButtonRow';
import SheetActionButton from '@/components/sheet/sheet-action-buttons/SheetActionButton';
import { LoadingOverlay } from '@/components/modal/LoadingOverlay';
import { useState } from 'react';
function ConfirmationModal({
title,
message,
onConfirm,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel'
}) {
const [isProcessing, setIsProcessing] = useState(false);
const handleConfirm = async () => {
setIsProcessing(true);
try {
await onConfirm();
p.close();
} catch (error) {
// Handle error
} finally {
setIsProcessing(false);
}
};
return (
<>
<Box padding="20px">
<Stack space="20px">
<Stack space="8px">
<Text size="20pt" weight="bold" color="label" align="center">
{title}
</Text>
<Text size="15pt" weight="regular" color="labelSecondary" align="center">
{message}
</Text>
</Stack>
<SheetActionButtonRow>
<SheetActionButton
label={cancelLabel}
onPress={p.close}
color="#8E8E93"
disabled={isProcessing}
/>
<SheetActionButton
label={confirmLabel}
onPress={handleConfirm}
color="#007AFF"
size="big"
disabled={isProcessing}
/>
</SheetActionButtonRow>
</Stack>
</Box>
{isProcessing && <LoadingOverlay title="Processing..." />}
</>
);
}
// Usage
function openConfirmation() {
p.open(
() => (
<ConfirmationModal
title="Delete Wallet?"
message="This action cannot be undone."
onConfirm={async () => {
await deleteWallet();
}}
confirmLabel="Delete"
/>
),
{ sheetHeight: 300 }
);
}
Alert Modal
import * as p from '@/screens/Portal';
import { Box, Stack, Text } from '@/design-system';
import SheetActionButton from '@/components/sheet/sheet-action-buttons/SheetActionButton';
function AlertModal({ title, message, buttonLabel = 'OK' }) {
return (
<Box padding="20px">
<Stack space="20px">
<Stack space="8px">
<Text size="20pt" weight="bold" color="label" align="center">
{title}
</Text>
<Text size="15pt" weight="regular" color="labelSecondary" align="center">
{message}
</Text>
</Stack>
<SheetActionButton
label={buttonLabel}
onPress={p.close}
color="#007AFF"
size="big"
/>
</Stack>
</Box>
);
}
// Usage
function showAlert(title: string, message: string) {
p.open(
() => <AlertModal title={title} message={message} />,
{ sheetHeight: 250 }
);
}
showAlert('Success', 'Your transaction was successful!');
Best Practices
1. Always Provide Loading Feedback
Show loading state for async operations:// ✅ Good - shows loading state
const handleAction = async () => {
setIsLoading(true);
try {
await operation();
} finally {
setIsLoading(false);
}
};
// ❌ Avoid - no loading feedback
const handleAction = async () => {
await operation();
};
2. Use Descriptive Loading Messages
Tell users what’s happening:// ✅ Good - descriptive
<LoadingOverlay title="Sending transaction..." />
// ❌ Avoid - generic
<LoadingOverlay title="Loading..." />
3. Handle Errors Gracefully
const handleAction = async () => {
setIsLoading(true);
try {
await operation();
} catch (error) {
showAlert('Error', error.message);
} finally {
setIsLoading(false);
}
};
4. Don’t Block UI Unnecessarily
Use loading overlays sparingly:// ✅ Good - loading overlay for critical operations
const sendTransaction = async () => {
setIsLoading(true);
await send();
setIsLoading(false);
};
// ❌ Avoid - blocking UI for background operations
const refreshData = async () => {
setIsLoading(true); // Don't block UI for refreshes
await fetchData();
setIsLoading(false);
};
// ✅ Better - use inline loading indicator
const refreshData = async () => {
setIsRefreshing(true);
await fetchData();
setIsRefreshing(false);
};
5. Cleanup Loading State
Always cleanup loading state, even on unmount:import { useEffect, useRef } from 'react';
function useAsyncOperation() {
const [isLoading, setIsLoading] = useState(false);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const execute = async (operation: () => Promise<void>) => {
setIsLoading(true);
try {
await operation();
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
};
return { isLoading, execute };
}
TouchableBackdrop
Used internally by LoadingOverlay on iOS to block touches:import TouchableBackdrop from '@/components/TouchableBackdrop';
// Blocks all touch events
<TouchableBackdrop disabled={false} />
// Allows touch events to pass through
<TouchableBackdrop disabled />