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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 9 additions & 6 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ jobs:
SKIP_ENV_VALIDATION: true
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Setup Node.js environment
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Cache pnpm modules
uses: actions/cache@v4
Expand All @@ -26,11 +29,11 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-

- name: Install pnpm
run: curl -f https://get.pnpm.io/v6.js | node - add --global pnpm

- name: Install project dependencies
run: pnpm install

- name: Run tests
run: pnpm run test

- name: Type check
run: pnpm check-types
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
node_modules
/.pnp
.pnp.js

# testing
/coverage
coverage

# next.js
/.next/
.next/
/out/

# production
Expand Down Expand Up @@ -42,6 +42,8 @@ export.sh
# typescript
*.tsbuildinfo

# turbo
.turbo

# Dev snapshots
dev/*.bz2
Expand Down
41 changes: 41 additions & 0 deletions apps/mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android
47 changes: 47 additions & 0 deletions apps/mobile/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"expo": {
"name": "Sarafu",
"slug": "sarafu",
"version": "0.1.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "sarafu",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "network.sarafu.app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"package": "network.sarafu.app"
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"expo-secure-store",
[
"expo-camera",
{
"cameraPermission": "Allow Sarafu to access your camera for QR code scanning."
}
]
],
"experiments": {
"typedRoutes": true
}
}
}
20 changes: 20 additions & 0 deletions apps/mobile/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StyleSheet, Text, View } from "react-native";

export default function LoginScreen() {
return (
<View style={styles.center}>
<Text style={styles.title}>Sign In</Text>
<Text style={styles.subtitle}>
SIWE (Sign-In with Ethereum) authentication will be implemented here.
The mobile app will use WalletConnect or a local wallet to sign a SIWE
message, then exchange it for a bearer token via the API.
</Text>
</View>
);
}

const styles = StyleSheet.create({
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 20 },
title: { fontSize: 24, fontWeight: "700", marginBottom: 16 },
subtitle: { fontSize: 15, color: "#6b7280", textAlign: "center", lineHeight: 22 },
});
48 changes: 48 additions & 0 deletions apps/mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { Tabs } from "expo-router";

function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>["name"];
color: string;
}) {
return <FontAwesome size={24} style={{ marginBottom: -3 }} {...props} />;
}

export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#16a34a",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Wallet",
tabBarIcon: ({ color }) => <TabBarIcon name="credit-card" color={color} />,
}}
/>
<Tabs.Screen
name="vouchers"
options={{
title: "Vouchers",
tabBarIcon: ({ color }) => <TabBarIcon name="th-large" color={color} />,
}}
/>
<Tabs.Screen
name="scan"
options={{
title: "Scan",
tabBarIcon: ({ color }) => <TabBarIcon name="qrcode" color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color }) => <TabBarIcon name="user" color={color} />,
}}
/>
</Tabs>
);
}
44 changes: 44 additions & 0 deletions apps/mobile/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { StyleSheet, Text, View } from "react-native";
import { useAuth } from "@/lib/auth";
import { useRouter } from "expo-router";

export default function WalletScreen() {
const { isAuthenticated, address } = useAuth();
const router = useRouter();

if (!isAuthenticated) {
return (
<View style={styles.center}>
<Text style={styles.title}>Sarafu Network</Text>
<Text style={styles.subtitle}>Sign in to view your wallet</Text>
<Text style={styles.link} onPress={() => router.push("/(auth)/login")}>
Sign In
</Text>
</View>
);
}

return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>My Wallet</Text>
<Text style={styles.address} numberOfLines={1}>
{address}
</Text>
</View>
<View style={styles.center}>
<Text style={styles.subtitle}>Token balances will appear here</Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff" },
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 20 },
header: { padding: 20, borderBottomWidth: 1, borderBottomColor: "#e5e7eb" },
title: { fontSize: 24, fontWeight: "700" },
subtitle: { fontSize: 16, color: "#6b7280", marginTop: 8 },
address: { fontSize: 13, color: "#9ca3af", marginTop: 4, fontFamily: "monospace" },
link: { fontSize: 16, color: "#16a34a", marginTop: 16, fontWeight: "600" },
});
51 changes: 51 additions & 0 deletions apps/mobile/app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { StyleSheet, Text, View } from "react-native";
import { useAuth } from "@/lib/auth";
import { useRouter } from "expo-router";

export default function ProfileScreen() {
const { isAuthenticated, address, signOut } = useAuth();
const router = useRouter();

if (!isAuthenticated) {
return (
<View style={styles.center}>
<Text style={styles.subtitle}>Sign in to view your profile</Text>
<Text style={styles.link} onPress={() => router.push("/(auth)/login")}>
Sign In
</Text>
</View>
);
}

return (
<View style={styles.container}>
<View style={styles.section}>
<Text style={styles.label}>Address</Text>
<Text style={styles.value} selectable>
{address}
</Text>
</View>

<Text
style={styles.signOut}
onPress={async () => {
await signOut();
router.replace("/");
}}
>
Sign Out
</Text>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff", padding: 20 },
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 20 },
section: { marginBottom: 24 },
label: { fontSize: 13, color: "#6b7280", marginBottom: 4, textTransform: "uppercase" },
value: { fontSize: 15, fontFamily: "monospace" },
subtitle: { fontSize: 16, color: "#6b7280" },
link: { fontSize: 16, color: "#16a34a", marginTop: 16, fontWeight: "600" },
signOut: { fontSize: 16, color: "#ef4444", marginTop: 32, fontWeight: "600" },
});
18 changes: 18 additions & 0 deletions apps/mobile/app/(tabs)/scan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { StyleSheet, Text, View } from "react-native";

export default function ScanScreen() {
return (
<View style={styles.center}>
<Text style={styles.title}>QR Scanner</Text>
<Text style={styles.subtitle}>
Camera-based QR scanning will be implemented here using expo-camera.
</Text>
</View>
);
}

const styles = StyleSheet.create({
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 20 },
title: { fontSize: 24, fontWeight: "700" },
subtitle: { fontSize: 16, color: "#6b7280", marginTop: 8, textAlign: "center" },
});
58 changes: 58 additions & 0 deletions apps/mobile/app/(tabs)/vouchers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from "react-native";
import { trpc } from "@/lib/trpc";

export default function VouchersScreen() {
const { data: vouchers, isLoading } = trpc.voucher.list.useQuery({
sortBy: "transactions",
sortDirection: "desc",
});

if (isLoading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#16a34a" />
</View>
);
}

return (
<View style={styles.container}>
<FlatList
data={vouchers ?? []}
keyExtractor={(item) => item.voucher_address}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.symbol}>{item.symbol}</Text>
<Text style={styles.name}>{item.voucher_name}</Text>
{item.location_name && (
<Text style={styles.location}>{item.location_name}</Text>
)}
</View>
)}
ListEmptyComponent={
<View style={styles.center}>
<Text style={styles.emptyText}>No vouchers found</Text>
</View>
}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff" },
center: { flex: 1, alignItems: "center", justifyContent: "center" },
list: { padding: 16, gap: 12 },
card: {
padding: 16,
borderRadius: 12,
backgroundColor: "#f9fafb",
borderWidth: 1,
borderColor: "#e5e7eb",
},
symbol: { fontSize: 18, fontWeight: "700" },
name: { fontSize: 14, color: "#374151", marginTop: 2 },
location: { fontSize: 12, color: "#9ca3af", marginTop: 4 },
emptyText: { fontSize: 16, color: "#6b7280" },
});
Loading
Loading