Add retry button to error states in equipment and subscription screens
- Added error state with retry functionality to equipment.tsx - Display error message when sensor loading fails - Provide "Try Again" button to retry loading - Clear error on successful retry - Added error state with retry functionality to subscription.tsx - Display error message when beneficiary loading fails - Provide "Try Again" button with icon to retry loading - Show offline icon and proper error layout - Added comprehensive tests for error handling - ErrorMessage component tests for inline errors - FullScreenError component tests - Equipment screen error state tests - Subscription screen error state tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
74a4c9e8f4
commit
88fc9042a7
79
__tests__/components/ErrorMessage.test.tsx
Normal file
79
__tests__/components/ErrorMessage.test.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent } from '@testing-library/react-native';
|
||||||
|
import { ErrorMessage, FullScreenError } from '@/components/ui/ErrorMessage';
|
||||||
|
|
||||||
|
describe('ErrorMessage', () => {
|
||||||
|
it('renders error message with retry button', () => {
|
||||||
|
const onRetry = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorMessage message="Test error" onRetry={onRetry} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Test error')).toBeTruthy();
|
||||||
|
expect(getByText('Retry')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onRetry when retry button is pressed', () => {
|
||||||
|
const onRetry = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorMessage message="Test error" onRetry={onRetry} />
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.press(getByText('Retry'));
|
||||||
|
expect(onRetry).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without retry button when onRetry is not provided', () => {
|
||||||
|
const { queryByText } = render(<ErrorMessage message="Test error" />);
|
||||||
|
|
||||||
|
expect(queryByText('Retry')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with skip button when onSkip is provided', () => {
|
||||||
|
const onSkip = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorMessage message="Test error" onSkip={onSkip} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Skip')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FullScreenError', () => {
|
||||||
|
it('renders full screen error with retry button', () => {
|
||||||
|
const onRetry = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<FullScreenError message="Connection failed" onRetry={onRetry} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Something went wrong')).toBeTruthy();
|
||||||
|
expect(getByText('Connection failed')).toBeTruthy();
|
||||||
|
expect(getByText('Try Again')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onRetry when Try Again button is pressed', () => {
|
||||||
|
const onRetry = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<FullScreenError message="Connection failed" onRetry={onRetry} />
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.press(getByText('Try Again'));
|
||||||
|
expect(onRetry).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders custom title', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<FullScreenError title="Custom Error" message="Test message" />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Custom Error')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without retry button when onRetry is not provided', () => {
|
||||||
|
const { queryByText } = render(
|
||||||
|
<FullScreenError message="Connection failed" />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Try Again')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
91
__tests__/screens/equipment.test.tsx
Normal file
91
__tests__/screens/equipment.test.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react-native';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@/services/api');
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useLocalSearchParams: () => ({ id: '1' }),
|
||||||
|
router: { back: jest.fn(), push: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.mock('@/contexts/BeneficiaryContext', () => ({
|
||||||
|
useBeneficiary: () => ({
|
||||||
|
currentBeneficiary: { id: 1, name: 'Test User' },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
jest.mock('@/contexts/BLEContext', () => ({
|
||||||
|
useBLE: () => ({
|
||||||
|
isBLEAvailable: true,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the screen after mocks are set up
|
||||||
|
import EquipmentScreen from '@/app/(tabs)/beneficiaries/[id]/equipment';
|
||||||
|
|
||||||
|
describe('EquipmentScreen - Error Handling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error message when API call fails', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Network error' } };
|
||||||
|
(api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue(mockError);
|
||||||
|
|
||||||
|
const { getByText, findByText } = render(<EquipmentScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Network error')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays retry button when error occurs', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Failed to load sensors' } };
|
||||||
|
(api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue(mockError);
|
||||||
|
|
||||||
|
const { findByText } = render(<EquipmentScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Try Again')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries loading sensors when retry button is pressed', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Failed to load sensors' } };
|
||||||
|
const mockSuccess = { ok: true, data: [] };
|
||||||
|
|
||||||
|
(api.getDevicesForBeneficiary as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockError)
|
||||||
|
.mockResolvedValueOnce(mockSuccess);
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(<EquipmentScreen />);
|
||||||
|
|
||||||
|
// Wait for error to appear
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Try Again')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Press retry button
|
||||||
|
const retryButton = await findByText('Try Again');
|
||||||
|
fireEvent.press(retryButton);
|
||||||
|
|
||||||
|
// Wait for error to disappear after successful retry
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByText('Failed to load sensors')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify API was called twice
|
||||||
|
expect(api.getDevicesForBeneficiary).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays generic error message for unknown errors', async () => {
|
||||||
|
(api.getDevicesForBeneficiary as jest.Mock).mockRejectedValue(
|
||||||
|
new Error('Unknown error')
|
||||||
|
);
|
||||||
|
|
||||||
|
const { findByText } = render(<EquipmentScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText(/Unknown error|Failed to load sensors/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
103
__tests__/screens/subscription.test.tsx
Normal file
103
__tests__/screens/subscription.test.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react-native';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@/services/api');
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useLocalSearchParams: () => ({ id: '1' }),
|
||||||
|
router: { back: jest.fn(), replace: jest.fn() },
|
||||||
|
}));
|
||||||
|
jest.mock('@/contexts/AuthContext', () => ({
|
||||||
|
useAuth: () => ({
|
||||||
|
user: { id: '1', email: 'test@example.com' },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
jest.mock('@stripe/stripe-react-native', () => ({
|
||||||
|
usePaymentSheet: () => ({
|
||||||
|
initPaymentSheet: jest.fn(),
|
||||||
|
presentPaymentSheet: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the screen after mocks are set up
|
||||||
|
import SubscriptionScreen from '@/app/(tabs)/beneficiaries/[id]/subscription';
|
||||||
|
|
||||||
|
describe('SubscriptionScreen - Error Handling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error message when beneficiary loading fails', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Failed to load beneficiary data' } };
|
||||||
|
(api.getWellNuoBeneficiary as jest.Mock).mockResolvedValue(mockError);
|
||||||
|
(api.getTransactionHistory as jest.Mock).mockResolvedValue({ ok: true, data: { transactions: [] } });
|
||||||
|
|
||||||
|
const { findByText } = render(<SubscriptionScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Failed to load beneficiary data')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays retry button when error occurs', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Network error' } };
|
||||||
|
(api.getWellNuoBeneficiary as jest.Mock).mockResolvedValue(mockError);
|
||||||
|
(api.getTransactionHistory as jest.Mock).mockResolvedValue({ ok: true, data: { transactions: [] } });
|
||||||
|
|
||||||
|
const { findByText } = render(<SubscriptionScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Try Again')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries loading beneficiary when retry button is pressed', async () => {
|
||||||
|
const mockError = { ok: false, error: { message: 'Network error' } };
|
||||||
|
const mockSuccess = {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test User',
|
||||||
|
subscription: { status: 'active' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
(api.getWellNuoBeneficiary as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockError)
|
||||||
|
.mockResolvedValueOnce(mockSuccess);
|
||||||
|
(api.getTransactionHistory as jest.Mock).mockResolvedValue({ ok: true, data: { transactions: [] } });
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(<SubscriptionScreen />);
|
||||||
|
|
||||||
|
// Wait for error to appear
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText('Try Again')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Press retry button
|
||||||
|
const retryButton = await findByText('Try Again');
|
||||||
|
fireEvent.press(retryButton);
|
||||||
|
|
||||||
|
// Wait for error to disappear after successful retry
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByText('Network error')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify API was called twice (initial + retry)
|
||||||
|
expect(api.getWellNuoBeneficiary).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays generic error message for exceptions', async () => {
|
||||||
|
(api.getWellNuoBeneficiary as jest.Mock).mockRejectedValue(
|
||||||
|
new Error('Connection timeout')
|
||||||
|
);
|
||||||
|
(api.getTransactionHistory as jest.Mock).mockResolvedValue({ ok: true, data: { transactions: [] } });
|
||||||
|
|
||||||
|
const { findByText } = render(<SubscriptionScreen />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findByText(/Connection timeout|Failed to load beneficiary data/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -46,6 +46,7 @@ export default function EquipmentScreen() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isDetaching, setIsDetaching] = useState<string | null>(null);
|
const [isDetaching, setIsDetaching] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const beneficiaryName = currentBeneficiary?.name || 'this person';
|
const beneficiaryName = currentBeneficiary?.name || 'this person';
|
||||||
|
|
||||||
@ -60,20 +61,20 @@ export default function EquipmentScreen() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
// Get WP sensors from API (attached to beneficiary)
|
// Get WP sensors from API (attached to beneficiary)
|
||||||
const response = await api.getDevicesForBeneficiary(id);
|
const response = await api.getDevicesForBeneficiary(id);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// If error is "Not authenticated with Legacy API" or network error,
|
setError(response.error?.message || 'Failed to load sensors');
|
||||||
// just show empty state without Alert
|
|
||||||
setApiSensors([]);
|
setApiSensors([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiSensors(response.data || []);
|
setApiSensors(response.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show empty state instead of Alert
|
setError(error instanceof Error ? error.message : 'Failed to load sensors');
|
||||||
setApiSensors([]);
|
setApiSensors([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -313,6 +314,20 @@ export default function EquipmentScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<View style={styles.errorContent}>
|
||||||
|
<Ionicons name="alert-circle" size={24} color={AppColors.error} />
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={styles.retryButton} onPress={handleRefresh}>
|
||||||
|
<Ionicons name="refresh" size={18} color={AppColors.white} />
|
||||||
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
@ -782,4 +797,41 @@ const styles = StyleSheet.create({
|
|||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
},
|
},
|
||||||
|
// Error Container
|
||||||
|
errorContainer: {
|
||||||
|
marginHorizontal: Spacing.lg,
|
||||||
|
marginVertical: Spacing.md,
|
||||||
|
padding: Spacing.lg,
|
||||||
|
backgroundColor: AppColors.errorLight,
|
||||||
|
borderRadius: BorderRadius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: AppColors.error,
|
||||||
|
},
|
||||||
|
errorContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.sm,
|
||||||
|
marginBottom: Spacing.md,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
color: AppColors.error,
|
||||||
|
fontWeight: FontWeights.medium,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: AppColors.error,
|
||||||
|
paddingVertical: Spacing.sm,
|
||||||
|
paddingHorizontal: Spacing.lg,
|
||||||
|
borderRadius: BorderRadius.md,
|
||||||
|
gap: Spacing.xs,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: AppColors.white,
|
||||||
|
fontSize: FontSizes.sm,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export default function SubscriptionScreen() {
|
|||||||
const [isCanceling, setIsCanceling] = useState(false);
|
const [isCanceling, setIsCanceling] = useState(false);
|
||||||
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
const [justSubscribed, setJustSubscribed] = useState(false);
|
const [justSubscribed, setJustSubscribed] = useState(false);
|
||||||
const [transactions, setTransactions] = useState<Array<{
|
const [transactions, setTransactions] = useState<Array<{
|
||||||
@ -73,12 +74,15 @@ export default function SubscriptionScreen() {
|
|||||||
const loadBeneficiary = async () => {
|
const loadBeneficiary = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
try {
|
try {
|
||||||
|
setError(null);
|
||||||
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
const response = await api.getWellNuoBeneficiary(parseInt(id, 10));
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
setBeneficiary(response.data);
|
setBeneficiary(response.data);
|
||||||
|
} else {
|
||||||
|
setError(response.error?.message || 'Failed to load beneficiary data');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Failed to load beneficiary
|
setError(error instanceof Error ? error.message : 'Failed to load beneficiary data');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -317,7 +321,7 @@ export default function SubscriptionScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!beneficiary) {
|
if (!beneficiary || error) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -328,7 +332,18 @@ export default function SubscriptionScreen() {
|
|||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Text style={styles.errorText}>Unable to load beneficiary</Text>
|
<Ionicons name="cloud-offline-outline" size={64} color={AppColors.textMuted} />
|
||||||
|
<Text style={styles.errorTitle}>{error || 'Unable to load beneficiary'}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.retryButton}
|
||||||
|
onPress={() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
loadBeneficiary();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="refresh" size={20} color={AppColors.white} />
|
||||||
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@ -568,6 +583,29 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
padding: Spacing.xl,
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
fontSize: FontSizes.lg,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
marginTop: Spacing.lg,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
paddingVertical: Spacing.sm + 4,
|
||||||
|
paddingHorizontal: Spacing.lg,
|
||||||
|
borderRadius: BorderRadius.lg,
|
||||||
|
marginTop: Spacing.xl,
|
||||||
|
gap: Spacing.sm,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: AppColors.white,
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
fontSize: FontSizes.base,
|
fontSize: FontSizes.base,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user