Skip to content
Open
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
114 changes: 112 additions & 2 deletions apps/playground/src/app/screens/NetworkTestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import {
} from 'react-native-nitro-websockets';
import { RootStackParamList } from '../navigation/types';
import { api, User, Post, Todo } from '../utils/network-activity/api';
import {
expoFetchApi,
type ExpoFetchDemoResult,
} from '../utils/network-activity/expo';
import {
nitroApi,
type NitroDemoResult,
Expand Down Expand Up @@ -792,6 +796,107 @@ const NitroHTTPTestComponent: React.FC = () => {
);
};

const ExpoFetchHTTPTestComponent: React.FC = () => {
const [isRunning, setIsRunning] = React.useState(false);
const [result, setResult] = React.useState<ExpoFetchDemoResult | null>(null);
const [error, setError] = React.useState<string | null>(null);

const runExpoFetchAction = React.useCallback(
async (action: () => Promise<ExpoFetchDemoResult>) => {
setIsRunning(true);
setError(null);

try {
const nextResult = await action();
setResult(nextResult);
} catch (actionError) {
setResult(null);
setError(
actionError instanceof Error
? actionError.message
: String(actionError),
);
} finally {
setIsRunning(false);
}
},
[],
);

return (
<ScrollView contentContainerStyle={styles.listContainer}>
<View style={styles.card}>
<Text style={styles.cardTitle}>Expo Fetch Test</Text>
<Text style={styles.cardBody}>
Runs requests through `expo/fetch`. Watch the Network Activity panel
for Expo source badges. Prefetch is not exposed by `expo/fetch`, so
this tab focuses on GET, POST, and abort handling.
</Text>

<View style={styles.nitroButtonGrid}>
<TouchableOpacity
style={[
styles.nitroButton,
isRunning && styles.refetchButtonDisabled,
]}
disabled={isRunning}
onPress={() => runExpoFetchAction(expoFetchApi.getUsers)}
>
<Text style={styles.nitroButtonText}>Expo GET</Text>
</TouchableOpacity>

<TouchableOpacity
style={[
styles.nitroButton,
isRunning && styles.refetchButtonDisabled,
]}
disabled={isRunning}
onPress={() => runExpoFetchAction(expoFetchApi.createPost)}
>
<Text style={styles.nitroButtonText}>Expo POST</Text>
</TouchableOpacity>

<TouchableOpacity
style={[
styles.nitroButton,
styles.nitroButtonDanger,
isRunning && styles.refetchButtonDisabled,
]}
disabled={isRunning}
onPress={() => runExpoFetchAction(expoFetchApi.abortSlowRequest)}
>
<Text style={styles.nitroButtonText}>Abort</Text>
</TouchableOpacity>
</View>

{isRunning && (
<View style={styles.nitroStatusRow}>
<ActivityIndicator size="small" color="#ffffff" />
<Text style={styles.nitroStatusText}>Running Expo request...</Text>
</View>
)}

{error && <Text style={styles.errorText}>Error: {error}</Text>}

{result && (
<View style={styles.responseContainer}>
<Text style={styles.responseTitle}>{result.title}</Text>
<Text style={styles.cardMeta}>
Status: {result.status} {result.statusText}
</Text>
{result.extra ? (
<Text style={styles.nitroExtraText}>{result.extra}</Text>
) : null}
<ScrollView style={styles.responseScrollView} nestedScrollEnabled>
<Text style={styles.responseText}>{result.body}</Text>
</ScrollView>
</View>
)}
</View>
</ScrollView>
);
};

const HTTPTestComponent: React.FC = () => {
const [activeTab, setActiveTab] = React.useState<
'users' | 'posts' | 'todos' | 'slow' | 'unreliable' | 'create' | 'large'
Expand Down Expand Up @@ -1775,6 +1880,7 @@ export const NetworkTestScreen: React.FC = () => {
const [activeTest, setActiveTest] = React.useState<
| 'http'
| 'nitro'
| 'expo-fetch'
| 'websocket'
| 'nitro-websocket'
| 'sse'
Expand Down Expand Up @@ -1828,8 +1934,8 @@ export const NetworkTestScreen: React.FC = () => {
<View style={styles.header}>
<Text style={styles.title}>Network Test</Text>
<Text style={styles.subtitle}>
Testing built-in HTTP, Nitro HTTP, built-in WebSocket, Nitro WebSocket,
and SSE connections
Testing built-in HTTP, Expo Fetch, Nitro HTTP, built-in WebSocket,
Nitro WebSocket, and SSE connections
</Text>

<TouchableOpacity
Expand All @@ -1842,6 +1948,7 @@ export const NetworkTestScreen: React.FC = () => {
<View style={styles.mainTabContainer}>
{[
{ key: 'http', label: 'HTTP Test' },
{ key: 'expo-fetch', label: 'Expo Fetch' },
{ key: 'nitro', label: 'Nitro HTTP' },
{ key: 'websocket', label: 'WebSocket Test' },
{ key: 'nitro-websocket', label: 'Nitro WS' },
Expand All @@ -1862,6 +1969,7 @@ export const NetworkTestScreen: React.FC = () => {
setActiveTest(
tab.key as
| 'http'
| 'expo-fetch'
| 'nitro'
| 'websocket'
| 'nitro-websocket'
Expand Down Expand Up @@ -1893,6 +2001,8 @@ export const NetworkTestScreen: React.FC = () => {
{renderHeader()}
{activeTest === 'http' ? (
<HTTPTestComponent />
) : activeTest === 'expo-fetch' ? (
<ExpoFetchHTTPTestComponent />
) : activeTest === 'nitro' ? (
<NitroHTTPTestComponent />
) : activeTest === 'websocket' ? (
Expand Down
107 changes: 107 additions & 0 deletions apps/playground/src/app/utils/network-activity/expo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { fetch as expoFetch } from 'expo/fetch';
import type { Post, User } from './api';

export type ExpoFetchDemoResult = {
title: string;
status: number;
statusText: string;
body: string;
extra?: string;
};

const prettyPrint = (value: unknown) => JSON.stringify(value, null, 2);

export const expoFetchApi = {
async getUsers(): Promise<ExpoFetchDemoResult> {
const response = await expoFetch(
'https://jsonplaceholder.typicode.com/users?_limit=3',
{
headers: {
'X-Rozenite-Test': 'expo-fetch-users',
},
},
);

if (!response.ok) {
throw new Error(`Expo fetch request failed with status ${response.status}`);
}

const users = (await response.json()) as User[];

return {
title: 'Expo GET users',
status: response.status,
statusText: response.statusText,
body: prettyPrint(users),
extra: `Fetched ${users.length} users via expo/fetch.`,
};
},

async createPost(): Promise<ExpoFetchDemoResult> {
const payload: Omit<Post, 'id'> = {
userId: 1,
title: 'Rozenite Expo fetch test post',
body: 'This request was created from the playground using expo/fetch.',
};

const response = await expoFetch(
'https://jsonplaceholder.typicode.com/posts?source=expo-fetch-playground',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Rozenite-Test': 'expo-fetch-create-post',
},
body: JSON.stringify(payload),
},
);

if (!response.ok) {
throw new Error(`Expo fetch request failed with status ${response.status}`);
}

const post = (await response.json()) as Post;

return {
title: 'Expo POST JSON',
status: response.status,
statusText: response.statusText,
body: prettyPrint(post),
extra: 'Creates a POST entry with an Expo source badge in Network Activity.',
};
},

async abortSlowRequest(): Promise<ExpoFetchDemoResult> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 250);

try {
await expoFetch('https://httpbin.org/delay/5', {
signal: controller.signal,
headers: {
'X-Rozenite-Test': 'expo-fetch-abort',
},
});

return {
title: 'Expo AbortController',
status: 200,
statusText: 'Unexpected success',
body: 'The delayed request completed before aborting.',
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);

return {
title: 'Expo AbortController',
status: 0,
statusText: 'Aborted',
body: message,
extra:
'Use this to verify failed Expo fetch requests show up in Network Activity.',
};
} finally {
clearTimeout(timeout);
}
},
};
Loading
Loading