Skip to content

Commit 67133ee

Browse files
authored
feat: implement API service layer and refactor network requests across screens (#408)
1 parent 1b66bad commit 67133ee

15 files changed

Lines changed: 226 additions & 333 deletions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import { View, Text, Image, ViewStyle, ImageStyle, StyleSheet } from 'react-native';
3+
import { COLORS } from '../theme/tokens';
4+
5+
type Props = {
6+
uri?: string | null;
7+
name?: string;
8+
size?: number;
9+
style?: ViewStyle | ImageStyle;
10+
};
11+
12+
export const Avatar: React.FC<Props> = ({ uri, name = 'D', size = 56, style }) => {
13+
const initials = name.charAt(0).toUpperCase();
14+
const imageStyle = [{ width: size, height: size, borderRadius: size / 2 } as ImageStyle, style as ImageStyle];
15+
const placeholderStyle = [{ width: size, height: size, borderRadius: size / 2, backgroundColor: COLORS.primary }, style as ViewStyle];
16+
17+
return uri ? (
18+
<Image source={{ uri }} style={imageStyle} />
19+
) : (
20+
<View style={[styles.placeholder, ...placeholderStyle]}>
21+
<Text style={[styles.placeholderText, { fontSize: Math.round(size / 2) }]}>{initials}</Text>
22+
</View>
23+
);
24+
};
25+
26+
export default Avatar;
27+
28+
const styles = StyleSheet.create({
29+
placeholder: {
30+
alignItems: 'center',
31+
justifyContent: 'center',
32+
},
33+
placeholderText: {
34+
color: COLORS.white,
35+
fontWeight: '800',
36+
},
37+
});

apps/mobile/src/config.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,18 @@ import * as Linking from 'expo-linking';
33

44
// DevCard API Configuration
55

6-
const getDevServerHost = () => {
7-
const constants = Constants as any;
8-
const hostUri =
9-
Constants.expoConfig?.hostUri ||
10-
constants.manifest2?.extra?.expoGo?.debuggerHost ||
11-
constants.manifest?.debuggerHost;
6+
// Prefer explicit configuration via Expo/EAS extras. Fallback to sensible defaults
7+
const extras = (Constants as any).manifest?.extra || (Constants as any).expoConfig?.extra;
128

13-
return hostUri?.split(':')[0] || '10.155.14.65';
14-
};
9+
const DEV_API = extras?.API_BASE_URL || extras?.DEV_API_BASE_URL;
10+
const DEV_APP = extras?.APP_URL;
1511

1612
export const API_BASE_URL = __DEV__
17-
? `http://${getDevServerHost()}:3000`
18-
: 'https://api.devcard.dev';
13+
? DEV_API ?? `http://10.0.2.2:3000` // 10.0.2.2 is a common emulator host for Android
14+
: extras?.API_BASE_URL ?? 'https://api.devcard.dev';
1915

2016
export const APP_URL = __DEV__
21-
? `http://${getDevServerHost()}:5173`
22-
: 'https://devcard.dev';
17+
? DEV_APP ?? `http://localhost:5173`
18+
: extras?.APP_URL ?? 'https://devcard.dev';
2319

2420
export const OAUTH_REDIRECT_URI = Linking.createURL('oauth/callback');

apps/mobile/src/context/AuthContext.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
2-
import { API_BASE_URL } from '../config';
2+
import { get } from '../services/api';
33

44
interface User {
55
id: string;
@@ -41,13 +41,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
4141
setToken(newToken);
4242
// TODO: Save token to secure storage
4343
try {
44-
const res = await fetch(`${API_BASE_URL}/api/profiles/me`, {
45-
headers: { Authorization: `Bearer ${newToken}` },
46-
});
47-
if (res.ok) {
48-
const userData = await res.json();
49-
setUser(userData);
50-
}
44+
const userData = await get<any>('/api/profiles/me', newToken).catch(() => null);
45+
if (userData) setUser(userData);
5146
} catch (error) {
5247
console.error('Failed to fetch user:', error);
5348
}
@@ -62,13 +57,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
6257
const refreshUser = async () => {
6358
if (!token) return;
6459
try {
65-
const res = await fetch(`${API_BASE_URL}/api/profiles/me`, {
66-
headers: { Authorization: `Bearer ${token}` },
67-
});
68-
if (res.ok) {
69-
const userData = await res.json();
70-
setUser(userData);
71-
}
60+
const userData = await get<any>('/api/profiles/me', token).catch(() => null);
61+
if (userData) setUser(userData);
7262
} catch (error) {
7363
console.error('Failed to refresh user:', error);
7464
}

apps/mobile/src/navigation/MainTabs.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ function TabIcon({ name, focused }: { name: string; focused: boolean }) {
6363
);
6464
}
6565

66+
function ScanButton() {
67+
return (
68+
<View style={styles.scanButton}>
69+
<Text style={styles.scanEmoji}>📷</Text>
70+
</View>
71+
);
72+
}
73+
6674
// ─── Tab Navigator ───
6775

6876
const Tab = createBottomTabNavigator<MainTabsParamList>();
@@ -87,11 +95,7 @@ function TabNavigator() {
8795
component={ScanScreen}
8896
options={{
8997
tabBarLabel: '',
90-
tabBarIcon: () => (
91-
<View style={styles.scanButton}>
92-
<Text style={styles.scanEmoji}>📷</Text>
93-
</View>
94-
),
98+
tabBarIcon: () => <ScanButton />,
9599
}}
96100
/>
97101
<Tab.Screen name="Cards" component={CardsScreen} />

apps/mobile/src/screens/CardsScreen.tsx

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useFocusEffect } from '@react-navigation/native';
1616
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens';
1717
import { useAuth } from '../context/AuthContext';
1818
import { PLATFORMS } from '@devcard/shared';
19-
import { API_BASE_URL } from '../config';
19+
import { get, post, del, put } from '../services/api';
2020
import { EmptyState } from '../components/EmptyState';
2121
import { Skeleton } from '../components/Skeleton';
2222

@@ -46,19 +46,12 @@ export default function CardsScreen() {
4646
const fetchData = useCallback(async (showLoading = true) => {
4747
if (showLoading) setLoading(true);
4848
try {
49-
const [cardsRes, profileRes] = await Promise.all([
50-
fetch(`${API_BASE_URL}/api/cards`, {
51-
headers: { Authorization: `Bearer ${token}` },
52-
}),
53-
fetch(`${API_BASE_URL}/api/profiles/me`, {
54-
headers: { Authorization: `Bearer ${token}` },
55-
}),
49+
const [cardsData, profileData] = await Promise.all([
50+
get<Card[]>('/api/cards', token).catch(() => []),
51+
get<any>('/api/profiles/me', token).catch(() => null),
5652
]);
57-
if (cardsRes.ok) setCards(await cardsRes.json());
58-
if (profileRes.ok) {
59-
const data = await profileRes.json();
60-
setAllLinks(data.platformLinks || []);
61-
}
53+
setCards(cardsData || []);
54+
setAllLinks(profileData?.platformLinks || []);
6255
} catch (error) {
6356
console.error('Failed to fetch:', error);
6457
} finally {
@@ -84,20 +77,11 @@ export default function CardsScreen() {
8477
return;
8578
}
8679
try {
87-
const res = await fetch(`${API_BASE_URL}/api/cards`, {
88-
method: 'POST',
89-
headers: {
90-
'Content-Type': 'application/json',
91-
Authorization: `Bearer ${token}`,
92-
},
93-
body: JSON.stringify({ title: newTitle.trim(), linkIds: selectedLinkIds }),
94-
});
95-
if (res.ok) {
96-
setShowCreate(false);
97-
setNewTitle('');
98-
setSelectedLinkIds([]);
99-
fetchData();
100-
}
80+
await post('/api/cards', { title: newTitle.trim(), linkIds: selectedLinkIds }, token);
81+
setShowCreate(false);
82+
setNewTitle('');
83+
setSelectedLinkIds([]);
84+
fetchData();
10185
} catch {
10286
Alert.alert('Error', 'Failed to create card');
10387
}
@@ -110,21 +94,23 @@ export default function CardsScreen() {
11094
text: 'Delete',
11195
style: 'destructive',
11296
onPress: async () => {
113-
await fetch(`${API_BASE_URL}/api/cards/${id}`, {
114-
method: 'DELETE',
115-
headers: { Authorization: `Bearer ${token}` },
116-
});
97+
try {
98+
await del(`/api/cards/${id}`, undefined, token);
99+
} catch {
100+
// ignore
101+
}
117102
fetchData();
118103
},
119104
},
120105
]);
121106
};
122107

123108
const setDefault = async (id: string) => {
124-
await fetch(`${API_BASE_URL}/api/cards/${id}/default`, {
125-
method: 'PUT',
126-
headers: { Authorization: `Bearer ${token}` },
127-
});
109+
try {
110+
await put(`/api/cards/${id}/default`, undefined, token);
111+
} catch {
112+
// ignore
113+
}
128114
fetchData();
129115
};
130116

apps/mobile/src/screens/ConnectPlatformsScreen.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
55
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens';
66
import { useAuth } from '../context/AuthContext';
77
import { API_BASE_URL } from '../config';
8+
import { get, del } from '../services/api';
89
import { LoadingPlaceholder } from '../components/LoadingPlaceholder';
910
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
1011
import type { RootStackParamList } from '../navigation/MainTabs';
@@ -28,13 +29,8 @@ export const ConnectPlatformsScreen: React.FC<Props> = ({ navigation: _navigatio
2829
return;
2930
}
3031
try {
31-
const response = await fetch(`${API_BASE_URL}/api/connect/status`, {
32-
headers: { Authorization: `Bearer ${token}` },
33-
});
34-
if (response.ok) {
35-
const data = await response.json();
36-
setConnectedPlatforms(data.connectedPlatforms || []);
37-
}
32+
const data = await get<any>('/api/connect/status', token).catch(() => null);
33+
setConnectedPlatforms(data?.connectedPlatforms || []);
3834
} catch (error) {
3935
console.error('Failed to fetch connected platforms', error);
4036
} finally {
@@ -79,15 +75,8 @@ export const ConnectPlatformsScreen: React.FC<Props> = ({ navigation: _navigatio
7975
onPress: async () => {
8076
try {
8177
if (!token) return;
82-
const response = await fetch(`${API_BASE_URL}/api/connect/${platform}`, {
83-
method: 'DELETE',
84-
headers: { Authorization: `Bearer ${token}` },
85-
});
86-
if (response.ok) {
87-
fetchConnections();
88-
} else {
89-
Alert.alert('Error', 'Failed to disconnect');
90-
}
78+
await del(`/api/connect/${platform}`, undefined, token);
79+
fetchConnections();
9180
} catch {
9281
Alert.alert('Error', 'Failed to disconnect');
9382
}

0 commit comments

Comments
 (0)