Skip to content
Merged
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
3 changes: 3 additions & 0 deletions __mocks__/@stellar/stellar-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export const Networks = {
};

export const Server = jest.fn(() => ({
root: jest.fn(async () => ({
network_passphrase: 'Test SDF Network ; September 2015',
})),
loadAccount: jest.fn(async () => ({ sequence: '0', balances: [] })),
submitTransaction: jest.fn(async () => ({ hash: 'mockhash' })),
payments: jest.fn(() => ({
Expand Down
92 changes: 54 additions & 38 deletions __tests__/NetworkStatusBanner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
* NetworkStatusBanner – component tests
*
* Acceptance criteria covered:
* AC-NB1 – Banner is not rendered when networkErrorType is 'none'.
* AC-NB1 – Banner is not rendered when state is 'online'.
* AC-NB2 – Banner is rendered for 'offline'.
* AC-NB3 – Banner is rendered for 'service-unavailable'.
* AC-NB4 – Retry button calls onRetry when tapped.
* AC-NB5 – Retry button is disabled while isRetrying is true.
* AC-NB6 – App does not crash when rendered with any valid props.
* AC-NB4 – Banner is rendered for 'wrong-network'.
* AC-NB5 – Retry button calls onRetry when tapped.
* AC-NB6 – Retry button is disabled while isRetrying is true.
* AC-NB7 – App does not crash when rendered with any valid state.
*/

import React from 'react';
Expand All @@ -16,22 +17,19 @@ import { render, fireEvent } from '@testing-library/react-native';
jest.mock('lucide-react-native', () => ({
WifiOff: () => null,
AlertTriangle: () => null,
CloudOff: () => null,
RefreshCw: () => null,
}));

import { NetworkStatusBanner } from '../src/components/NetworkStatusBanner';

const OFFLINE_MESSAGE = 'No internet connection. Check your network and try again.';
const SERVICE_MESSAGE = 'Stellar Testnet services appear unavailable. Please try again shortly.';
// ─── AC-NB1: Hidden when online ────────────────────────────────────────────

// ─── AC-NB1: Hidden when no error ────────────────────────────────────────────

describe('AC-NB1 – not rendered when networkErrorType is none', () => {
it('returns null when type is "none"', () => {
describe('AC-NB1 – not rendered when state is online', () => {
it('returns null when state is "online"', () => {
const { queryByTestId } = render(
<NetworkStatusBanner
networkErrorType="none"
message=""
state="online"
onRetry={jest.fn()}
/>
);
Expand All @@ -42,44 +40,56 @@ describe('AC-NB1 – not rendered when networkErrorType is none', () => {
// ─── AC-NB2: Offline banner ───────────────────────────────────────────────────

describe('AC-NB2 – offline banner', () => {
it('renders the banner and message when type is "offline"', () => {
it('renders the banner and message when state is "offline"', () => {
const { getByTestId, getByText } = render(
<NetworkStatusBanner
networkErrorType="offline"
message={OFFLINE_MESSAGE}
state="offline"
onRetry={jest.fn()}
/>
);
expect(getByTestId('network-status-banner')).toBeTruthy();
expect(getByText(OFFLINE_MESSAGE)).toBeTruthy();
expect(getByText(/You are offline/i)).toBeTruthy();
});
});

// ─── AC-NB3: Service-unavailable banner ──────────────────────────────────────

describe('AC-NB3 – service-unavailable banner', () => {
it('renders the banner and message when type is "service-unavailable"', () => {
it('renders the banner and message when state is "service-unavailable"', () => {
const { getByTestId, getByText } = render(
<NetworkStatusBanner
state="service-unavailable"
onRetry={jest.fn()}
/>
);
expect(getByTestId('network-status-banner')).toBeTruthy();
expect(getByText(/Stellar network services are unavailable/i)).toBeTruthy();
});
});

// ─── AC-NB4: Wrong-network banner ────────────────────────────────────────────

describe('AC-NB4 – wrong-network banner', () => {
it('renders the banner and message when state is "wrong-network"', () => {
const { getByTestId, getByText } = render(
<NetworkStatusBanner
networkErrorType="service-unavailable"
message={SERVICE_MESSAGE}
state="wrong-network"
onRetry={jest.fn()}
/>
);
expect(getByTestId('network-status-banner')).toBeTruthy();
expect(getByText(SERVICE_MESSAGE)).toBeTruthy();
expect(getByText(/Connected to the wrong blockchain network/i)).toBeTruthy();
});
});

// ─── AC-NB4: Retry callback ───────────────────────────────────────────────────
// ─── AC-NB5: Retry callback ───────────────────────────────────────────────────

describe('AC-NB4 – retry button calls onRetry', () => {
describe('AC-NB5 – retry button calls onRetry', () => {
it('calls onRetry when the retry button is pressed', () => {
const onRetry = jest.fn();
const { getByTestId } = render(
<NetworkStatusBanner
networkErrorType="offline"
message={OFFLINE_MESSAGE}
state="offline"
onRetry={onRetry}
/>
);
Expand All @@ -88,35 +98,32 @@ describe('AC-NB4 – retry button calls onRetry', () => {
});
});

// ─── AC-NB5: Retry disabled while retrying ───────────────────────────────────
// ─── AC-NB6: Retry disabled while retrying ───────────────────────────────────

describe('AC-NB5 – retry button is disabled while isRetrying', () => {
describe('AC-NB6 – retry button is disabled while isRetrying', () => {
it('renders retry button as disabled when isRetrying is true', () => {
const onRetry = jest.fn();
const { getByTestId } = render(
<NetworkStatusBanner
networkErrorType="offline"
message={OFFLINE_MESSAGE}
state="offline"
onRetry={onRetry}
isRetrying={true}
/>
);
const retryBtn = getByTestId('network-status-retry');
// The button should not respond to presses when disabled
fireEvent.press(retryBtn);
expect(onRetry).not.toHaveBeenCalled();
});
});

// ─── AC-NB6: Does not crash ───────────────────────────────────────────────────
// ─── AC-NB7: Does not crash ───────────────────────────────────────────────────

describe('AC-NB6 – does not crash with any valid props', () => {
describe('AC-NB7 – does not crash with any valid state', () => {
it('renders without crashing for "offline"', () => {
expect(() =>
render(
<NetworkStatusBanner
networkErrorType="offline"
message={OFFLINE_MESSAGE}
state="offline"
onRetry={jest.fn()}
/>
)
Expand All @@ -127,20 +134,29 @@ describe('AC-NB6 – does not crash with any valid props', () => {
expect(() =>
render(
<NetworkStatusBanner
networkErrorType="service-unavailable"
message={SERVICE_MESSAGE}
state="service-unavailable"
onRetry={jest.fn()}
/>
)
).not.toThrow();
});

it('renders without crashing for "wrong-network"', () => {
expect(() =>
render(
<NetworkStatusBanner
state="wrong-network"
onRetry={jest.fn()}
/>
)
).not.toThrow();
});

it('renders without crashing for "none"', () => {
it('renders without crashing for "online"', () => {
expect(() =>
render(
<NetworkStatusBanner
networkErrorType="none"
message=""
state="online"
onRetry={jest.fn()}
/>
)
Expand Down
12 changes: 6 additions & 6 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { Tabs } from "expo-router";
import { Home, History, PiggyBank, Settings } from "lucide-react-native";
import { useTheme } from "../../src/hooks/useTheme";
import { useNetworkState } from "../../src/hooks/useNetworkState";
import { NetworkStateBanner } from "../../src/components/NetworkStateBanner";
import { NetworkStatusBanner } from "../../src/components/NetworkStatusBanner";

export default function TabsLayout() {
const { colors } = useTheme();
const { state: networkState, retry } = useNetworkState();

return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<NetworkStateBanner state={networkState} onRetry={retry} />
<NetworkStatusBanner state={networkState} onRetry={retry} />
<Tabs
screenOptions={{
headerStyle: {
Expand All @@ -36,14 +36,14 @@ export default function TabsLayout() {
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
tabBarIcon: ({ color, size }: { color: string; size: number }) => <Home color={color} size={size} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Activity",
tabBarIcon: ({ color, size }) => (
tabBarIcon: ({ color, size }: { color: string; size: number }) => (
<History color={color} size={size} />
),
}}
Expand All @@ -52,7 +52,7 @@ export default function TabsLayout() {
name="vault"
options={{
title: "Vault",
tabBarIcon: ({ color, size }) => (
tabBarIcon: ({ color, size }: { color: string; size: number }) => (
<PiggyBank color={color} size={size} />
),
}}
Expand All @@ -61,7 +61,7 @@ export default function TabsLayout() {
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color, size }) => (
tabBarIcon: ({ color, size }: { color: string; size: number }) => (
<Settings color={color} size={size} />
),
}}
Expand Down
28 changes: 19 additions & 9 deletions app/(tabs)/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useWalletStore, TransactionRecord } from '../../src/store/walletStore';
import { RADIUS, SIZES, ThemeColors } from '../../src/constants/theme';
import { useTheme } from '../../src/hooks/useTheme';
import { TransactionListItem } from '../../src/components/TransactionListItem';
import { NetworkStateBanner } from '../../src/components/NetworkStateBanner';
import { NetworkStatusBanner } from '../../src/components/NetworkStatusBanner';
import { EmptyState } from '../../src/components/EmptyState';
import { WalletEmptyState } from '../../src/components/WalletEmptyState';
import { LoadingState } from '../../src/components/LoadingState';
Expand All @@ -40,13 +40,19 @@ type FilterType = (typeof FILTERS)[number]['value'];
* Footer rendered below the list while loading more items or when the
* end-of-list has been reached.
*/
const ListFooter: React.FC<{
const ListFooter = ({
isLoadingMore,
hasMoreTransactions,
hasTransactions,
colors,
styles,
}: {
isLoadingMore: boolean;
hasMoreTransactions: boolean;
hasTransactions: boolean;
colors: ThemeColors;
styles: ReturnType<typeof createStyles>;
}> = ({ isLoadingMore, hasMoreTransactions, hasTransactions, colors, styles }) => {
}) => {
if (!hasTransactions) return null;

if (isLoadingMore) {
Expand Down Expand Up @@ -74,11 +80,15 @@ const ListFooter: React.FC<{
/**
* Shown when there are no transactions and the screen is not loading.
*/
const ActivityEmptyState: React.FC<{
const ActivityEmptyState = ({
colors,
styles,
onReceivePress,
}: {
colors: ThemeColors;
styles: ReturnType<typeof createStyles>;
onReceivePress: () => void;
}> = ({ colors, styles, onReceivePress }) => (
}) => (
<View style={styles.emptyState} testID="empty-state">
<EmptyState
icon={<Clock color={colors.textMuted} size={48} />}
Expand Down Expand Up @@ -135,7 +145,7 @@ export default function HistoryScreen() {
}

const filteredTransactions = useMemo(() => {
return transactions.filter(tx => {
return transactions.filter((tx: TransactionRecord) => {
if (filter === 'all') return true;

const isSent = tx.from === publicKey;
Expand Down Expand Up @@ -165,7 +175,7 @@ export default function HistoryScreen() {
transaction={item}
currentPublicKey={publicKey}
variant="card"
onPress={(tx) => router.push(`/transaction/${tx.id}`)}
onPress={(tx: TransactionRecord) => router.push(`/transaction/${tx.id}`)}
/>
),
[publicKey, router]
Expand Down Expand Up @@ -233,7 +243,7 @@ export default function HistoryScreen() {
onEndReachedThreshold={0.2}
ListHeaderComponent={
<>
<NetworkStateBanner
<NetworkStatusBanner
state={networkState}
onRetry={() => { refreshWalletData(); retry(); }}
isRetrying={isLoading}
Expand Down Expand Up @@ -347,7 +357,7 @@ const createStyles = (colors: ThemeColors) => StyleSheet.create({
filterChip: {
paddingHorizontal: SIZES.md,
paddingVertical: SIZES.sm,
borderRadius: RADIUS.full,
borderRadius: RADIUS.round,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.surface,
Expand Down
4 changes: 2 additions & 2 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useTheme } from '../../src/hooks/useTheme';
import { Button } from '../../src/components/Button';
import { FundButton } from '../../src/components/FundButton';
import { TransactionListItem } from '../../src/components/TransactionListItem';
import { NetworkStateBanner } from '../../src/components/NetworkStateBanner';
import { NetworkStatusBanner } from '../../src/components/NetworkStatusBanner';
import { WalletEmptyState } from '../../src/components/WalletEmptyState';
import { BalanceDisplay } from '../../src/components/BalanceDisplay';
import { FundingStatusBanner } from '../../src/components/FundingStatusBanner';
Expand Down Expand Up @@ -80,7 +80,7 @@ export default function HomeScreen() {
/>
}
>
<NetworkStateBanner
<NetworkStatusBanner
state={networkState}
onRetry={handleRetry}
isRetrying={isLoading}
Expand Down
9 changes: 5 additions & 4 deletions app/(tabs)/vault.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { VaultReceiptModal } from "../../src/components/VaultReceiptModal";
import { isActionSupported, getActionUnsupportedReason, getActionUnsupportedDetail } from '../../src/utils/vaultCapabilities';
import { useNetworkState } from '../../src/hooks/useNetworkState';
import { NetworkStateBanner } from '../../src/components/NetworkStateBanner';
import { NetworkStatusBanner } from '../../src/components/NetworkStatusBanner';
import { WithdrawalPreview } from '../../src/features/vault/WithdrawalPreview';
import { DepositPreview } from '../../src/features/vault/DepositPreview';
import type { VaultLock } from '../../src/types';

const LOCK_PERIOD_SECONDS = 30 * 24 * 60 * 60; // 30 days
const VAULT_INTRO_SEEN_KEY = '@pocketpay_vault_intro_seen';
Expand All @@ -45,11 +46,11 @@ export default function VaultScreen() {
const { state: networkState, disableWriteActions: networkDisabled, retry: retryNetwork } = useNetworkState({ error: walletError });
const {
balance,
locks,
locks: _unused_locks,
isConfigured,
contractId,
isLoadingBalance,
isLoadingLocks,
isLoadingLocks: _unused_isLoadingLocks,
isSubmitting,
balanceError,
vaultError,
Expand Down Expand Up @@ -276,7 +277,7 @@ export default function VaultScreen() {
onClose={() => setReceiptVisible(false)}
/>

<NetworkStateBanner
<NetworkStatusBanner
state={networkState}
onRetry={() => {
retryNetwork();
Expand Down
Loading