Skip to content

Commit 8e6d6a8

Browse files
committed
Major feature release: Contacts, Price, SPV wiring, 9 phases complete
Phase 1 - Contacts / Address Book: - Contacts table in SQLite with full CRUD - Contacts screen (search, add, edit, delete with emoji avatars) - ContactPicker modal integrated in send screen - Recent recipients tracking + quick-select in send screen - Save-to-contacts prompt after sending Phase 2 - Price Display: - Price service polling explorer.fairco.in/api/price every 60s - USD equivalent shown on home screen and send screen - 24h price change indicator (green/red) Phase 3 - SPV Client Wiring: - SocketProvider implementations (native TCP, Electron IPC, fallback) - HeaderStore adapter (SQLite <-> SPV client) - SPV client started on wallet initialization (peer discovery, header sync) - Bloom filter loaded with wallet addresses - Real sendTransaction: buildTransaction -> signInput -> serialize -> broadcast - Explorer API fallback when SPV unavailable Phase 4 - Transaction Details: - Full tx detail screen with type badge, amount, timestamp, confirmations - Editable transaction notes (persisted to SQLite) - Copy txid, view on explorer, add to contacts - TransactionItem is now tappable (navigates to detail) Phase 5 - Explorer APIs (separate repo): - Price API with admin-set prices and history (GET/POST /api/price) - Address balance/UTXOs/txs via RPC (GET /api/address/:addr) - Transaction broadcast (POST /api/tx/broadcast) - Fee estimation (GET /api/fee-estimate) Phase 6 - UX Polish: - Auto-lock timer (configurable 1-30 min, tracks AppState) - Share payment request via system share sheet - Currency selector (USD/EUR/BTC) in settings - Address labels and transaction notes tables Phase 7 - Security: - BIP38 encrypted key export UI (PIN-protected) - Wallet metadata backup export/import (JSON, no private keys) Phase 8 - Advanced: - Network switching (mainnet <-> testnet) with SPV restart - Watch-only wallets (import xpub, view-only) - Coin control (select specific UTXOs for transactions) Phase 9 - i18n: - English + Spanish translations (~60 keys) - Device locale auto-detection - Applied to web tabs, home screen, receive screen
1 parent bbe9d5f commit 8e6d6a8

25 files changed

Lines changed: 3745 additions & 112 deletions

app/(tabs)/_layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
* Tab layout for Android and iOS using native system tab bar.
33
* Uses NativeTabs from expo-router for platform-native look and feel.
44
* Material symbols (md) for Android, SF Symbols (sf) for iOS.
5+
*
6+
* Note: NativeTabs labels are static strings set at render time.
7+
* For full i18n support, these would need to use translated values
8+
* from the i18n system. The web tab layout (_layout.web.tsx) already
9+
* uses t() for translated labels. NativeTabs i18n requires passing
10+
* the translated strings directly to <NativeTabs.Trigger.Label>.
511
*/
612

713
import { ThemeProvider, DarkTheme } from "@react-navigation/native";

app/(tabs)/_layout.web.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
* Tab layout for Web (Electron desktop).
33
* Uses headless tabs from expo-router/ui with custom styling.
44
* NativeTabs is not available on web, so we use the JS-based approach.
5+
* Tab labels use the i18n system for translation.
56
*/
67

78
import { Tabs, TabList, TabTrigger, TabSlot } from "expo-router/ui";
89
import { View, Text, StyleSheet } from "react-native";
910
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
11+
import { t } from "../../src/i18n";
1012

1113
interface WebTabProps {
1214
name: string;
@@ -32,10 +34,10 @@ export default function TabLayout() {
3234
<TabSlot />
3335
</View>
3436
<TabList style={styles.tabList}>
35-
<WebTab name="index" href="/(tabs)" icon="wallet" label="Wallet" />
36-
<WebTab name="send" href="/(tabs)/send" icon="arrow-up-bold" label="Send" />
37-
<WebTab name="receive" href="/(tabs)/receive" icon="arrow-down-bold" label="Receive" />
38-
<WebTab name="settings" href="/(tabs)/settings" icon="cog" label="Settings" />
37+
<WebTab name="index" href="/(tabs)" icon="wallet" label={t("wallet.title")} />
38+
<WebTab name="send" href="/(tabs)/send" icon="arrow-up-bold" label={t("wallet.send")} />
39+
<WebTab name="receive" href="/(tabs)/receive" icon="arrow-down-bold" label={t("wallet.receive")} />
40+
<WebTab name="settings" href="/(tabs)/settings" icon="cog" label={t("wallet.settings")} />
3941
</TabList>
4042
</Tabs>
4143
</View>

app/(tabs)/index.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
* Displays balance, sync status, and recent transactions.
44
*/
55

6-
import { useCallback } from "react";
6+
import { useCallback, useState } from "react";
77
import { View, Text, ScrollView, RefreshControl } from "react-native";
88
import { SafeAreaView } from "react-native-safe-area-context";
9+
import { useFocusEffect } from "expo-router";
910
import { useWalletStore } from "../../src/wallet/wallet-store";
1011
import { SyncStatus } from "../../src/ui/components/SyncStatus";
1112
import { TransactionItem } from "../../src/ui/components/TransactionItem";
13+
import {
14+
startPricePolling,
15+
stopPricePolling,
16+
getCachedPrice,
17+
type PriceData,
18+
} from "../../src/services/price";
19+
import { t } from "../../src/i18n";
1220

1321
/** Format satoshis (bigint) to FAIR display string */
1422
function formatBalance(sats: bigint): string {
@@ -18,6 +26,26 @@ function formatBalance(sats: bigint): string {
1826
return `${whole.toString()}.${fracStr}`;
1927
}
2028

29+
/** Format a FAIR amount (from bigint sats) to a fiat value string */
30+
function formatFiat(sats: bigint, rate: number): string {
31+
const fair = Number(sats) / 100_000_000;
32+
return (fair * rate).toFixed(2);
33+
}
34+
35+
/** Format 24h change percentage with sign and color class */
36+
function formatChange(change: number | null): {
37+
text: string;
38+
colorClass: string;
39+
} {
40+
if (change === null) {
41+
return { text: "", colorClass: "text-fair-muted" };
42+
}
43+
const sign = change >= 0 ? "+" : "";
44+
const text = `${sign}${change.toFixed(1)}%`;
45+
const colorClass = change >= 0 ? "text-green-400" : "text-red-400";
46+
return { text, colorClass };
47+
}
48+
2149
export default function HomeScreen() {
2250
const balance = useWalletStore((s) => s.balance);
2351
const isSyncing = useWalletStore((s) => s.isSyncing);
@@ -29,14 +57,31 @@ export default function HomeScreen() {
2957
const refreshBalance = useWalletStore((s) => s.refreshBalance);
3058
const loading = useWalletStore((s) => s.loading);
3159

60+
const [price, setPrice] = useState<PriceData | null>(getCachedPrice);
61+
3262
const displayBalance = formatBalance(balance);
3363

64+
// Start/stop price polling when tab is focused
65+
useFocusEffect(
66+
useCallback(() => {
67+
startPricePolling((updated) => {
68+
setPrice(updated);
69+
});
70+
return () => {
71+
stopPricePolling();
72+
};
73+
}, []),
74+
);
75+
3476
const handleRefresh = useCallback(() => {
3577
refreshBalance();
3678
}, [refreshBalance]);
3779

3880
const recentTransactions = transactions.slice(0, 5);
3981

82+
const usdValue = price ? formatFiat(balance, price.usd) : null;
83+
const change = price ? formatChange(price.change24h) : null;
84+
4085
return (
4186
<SafeAreaView className="flex-1 bg-fair-dark" edges={["top", "left", "right"]}>
4287
<ScrollView
@@ -63,11 +108,25 @@ export default function HomeScreen() {
63108

64109
{/* Balance */}
65110
<View className="items-center px-6 pt-8 pb-6">
66-
<Text className="text-fair-muted text-sm mb-2">Total Balance</Text>
111+
<Text className="text-fair-muted text-sm mb-2">{t("wallet.balance")}</Text>
67112
<Text className="text-white text-4xl font-bold tracking-tight">
68113
{displayBalance}
69114
</Text>
70115
<Text className="text-fair-green text-lg mt-1">FAIR</Text>
116+
117+
{/* USD equivalent and 24h change */}
118+
{usdValue !== null ? (
119+
<View className="flex-row items-center mt-2 gap-2">
120+
<Text className="text-fair-muted text-sm">
121+
{"\u2248"} ${usdValue} USD
122+
</Text>
123+
{change?.text ? (
124+
<Text className={`text-sm font-medium ${change.colorClass}`}>
125+
{change.text}
126+
</Text>
127+
) : null}
128+
</View>
129+
) : null}
71130
</View>
72131

73132
{/* Sync status */}
@@ -100,6 +159,7 @@ export default function HomeScreen() {
100159
{recentTransactions.map((tx) => (
101160
<TransactionItem
102161
key={tx.txid}
162+
txid={tx.txid}
103163
type={tx.type}
104164
amount={formatBalance(tx.amount < 0n ? -tx.amount : tx.amount)}
105165
address={tx.address}

app/(tabs)/receive.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
ActivityIndicator,
1414
ScrollView,
1515
FlatList,
16+
Share,
1617
} from "react-native";
1718
import { SafeAreaView } from "react-native-safe-area-context";
1819
import * as Clipboard from "expo-clipboard";
1920
import QRCode from "react-native-qrcode-svg";
2021
import { useWalletStore } from "../../src/wallet/wallet-store";
2122
import { Button } from "../../src/ui/components/Button";
23+
import { t } from "../../src/i18n";
2224

2325
function truncateAddress(address: string): string {
2426
if (address.length <= 16) return address;
@@ -43,6 +45,14 @@ export default function ReceiveScreen() {
4345
setSelectedAddress(addr);
4446
}, [getNewAddress]);
4547

48+
const handleShare = useCallback(async () => {
49+
const uri = `faircoin:${displayAddress}`;
50+
await Share.share({
51+
message: `Pay me with FairCoin:\n${uri}`,
52+
title: "FairCoin Payment Request",
53+
});
54+
}, [displayAddress]);
55+
4656
const handleSelectAddress = useCallback(
4757
(address: string) => {
4858
setSelectedAddress(address);
@@ -76,7 +86,7 @@ export default function ReceiveScreen() {
7686
>
7787
{/* Title */}
7888
<Text className="text-white text-xl font-bold mb-1 text-center">
79-
Receive FAIR
89+
{t("receive.title")}
8090
</Text>
8191
<Text className="text-fair-muted text-sm mb-6 text-center">
8292
Share this address to receive FairCoin
@@ -110,12 +120,17 @@ export default function ReceiveScreen() {
110120
{/* Actions */}
111121
<View className="w-full gap-3 mb-8">
112122
<Button
113-
title="Copy Address"
123+
title={t("receive.copy")}
114124
onPress={handleCopy}
115125
variant="primary"
116126
/>
117127
<Button
118-
title="New Address"
128+
title={t("receive.share")}
129+
onPress={handleShare}
130+
variant="outline"
131+
/>
132+
<Button
133+
title={t("receive.new_address")}
119134
onPress={handleNewAddress}
120135
variant="outline"
121136
/>

0 commit comments

Comments
 (0)