Skip to content

Commit 1cf15e0

Browse files
authored
Build Transaction History and Receipt System (#140)
* feat(sep0007): add SEP-0007 URI parser and validator - parseSep0007Uri: parses web+stellar:pay and web+stellar:tx URIs - buildSep0007PayUri: builds a SEP-0007 pay URI from params - detectQrContentType: classifies scanned QR content - isStellarAddress / isFederatedAddress / isValidStellarDestination helpers - Full TypeScript types for pay and tx operation params * feat(hooks): add useCameraPermission hook Wraps expo-camera's useCameraPermissions with a clean interface: - granted / denied / undetermined state flags - loading state during permission request - requestPermission() async helper * feat(scan): implement QR code scanner with SEP-0007 support - Live camera feed via expo-camera CameraView - Barcode scanner restricted to QR type - Flash toggle (on/off) - Gallery picker via expo-image-picker - Manual address / URI entry fallback - SEP-0007 pay and tx URI parsing with payment preview - Plain Stellar G-address and federated address detection - Payment details preview screen (recipient, amount, asset, memo) - Confirm → navigates to /transfer pre-filled with scanned params - Graceful error screen for invalid QR codes with retry - Camera permission request and denied state with Settings deep-link - Responsive scanner viewport (tablet-aware) - Corner bracket overlay for scan target UX - Accessibility labels and roles throughout * feat(receive): generate real SEP-0007 QR code for receiving payments - Replace QR placeholder icon with react-native-qrcode-svg - External wallet receive generates a web+stellar:pay URI via buildSep0007PayUri - BLINKS ID receive shows blinkId as QR value - Wire up Copy ID button with Clipboard + toast feedback - Import buildSep0007PayUri from sep0007 utils * feat(merchant/qr-code): generate SEP-0007 compliant payment QR - Build web+stellar:pay URI with destination, amount, asset_code, memo - Replace JSON payload with standards-compliant SEP-0007 URI - Add Share functionality to share the URI directly - Any SEP-0007 compatible wallet can now scan and pay the merchant * chore(deps): install expo-camera and expo-image-picker - expo-camera: live camera feed + QR barcode scanning - expo-image-picker: gallery access for QR image selection * feat(types): add Transaction, TransactionPage, TransactionFilters types Defines the full data model for the transaction history system: - Transaction: id, type, status, amount, asset, fiatValue, address, stellarTxHash, memo, fee, network - TransactionPage: paginated response with nextCursor and total - TransactionFilters: type, status, dateFrom, dateTo, search, amountMin/Max * feat(service): add transactionService with pagination and offline cache - fetchTransactions: cursor-based pagination with filter support (type, status, date range, amount range, full-text search) - fetchTransactionById: cache-first single transaction lookup - invalidateTransactionCache: clears AsyncStorage cache - 5-minute TTL cache via AsyncStorage - Mock data with 8 realistic Stellar transactions (replace with API) * feat(utils): add receiptGenerator for shareable transaction receipts - buildReceiptText: formats a plain-text receipt with all tx details - shareReceipt: writes receipt to cache dir and opens native share sheet - getReceiptText: returns receipt string for display/copy - Includes Stellar explorer URL in receipt when hash is present * feat(hooks): add useTransactions hook - Initial load, pull-to-refresh, infinite scroll (loadMore) - applyFilters: updates filters and reloads from page 1 - Separate loading / refreshing / loadingMore states - Error state with message - Cursor ref tracking for pagination * feat(components): add TransactionFilterSheet bottom sheet - Modal bottom sheet with drag handle - Type filter chips (All / Sent / Received) - Status filter chips (All / Completed / Pending / Failed) - Date range inputs (dateFrom / dateTo) - Amount range inputs (min / max) - Reset and Apply buttons - Syncs with current filters when opened * feat(history): rewrite history screen with full transaction system - Paginated list via useTransactions hook - Date-grouped sections (Today / Yesterday / date label) - Pull-to-refresh with RefreshControl - Infinite scroll with onEndReached - Debounced search bar (address, memo, hash, asset) - Filter button with active indicator dot - TransactionFilterSheet integration - Transaction row: icon, label, time, status pill, amount, fiat value - Loading, error, and empty states - Tapping a row navigates to /transaction/[id] * feat(transaction): add transaction detail and receipt screen - Amount hero with type icon, fiat value, status badge - Transaction details card: date, type, counterparty, address, memo, fee, network - Blockchain card: truncated hash with copy button, View on Stellar Explorer link - Share Receipt button: generates plain-text receipt and opens native share sheet - Share icon in header for quick access - Handles loading and not-found states - Deep-linkable via /transaction/[id] * chore(deps): install expo-sharing, expo-file-system, expo-clipboard Required for receipt generation and sharing functionality
1 parent c68d4f6 commit 1cf15e0

9 files changed

Lines changed: 1834 additions & 194 deletions

File tree

mobileapp/app/(personal)/history.tsx

Lines changed: 500 additions & 184 deletions
Large diffs are not rendered by default.

mobileapp/app/transaction/[id].tsx

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.

mobileapp/package-lock.json

Lines changed: 33 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobileapp/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
"@react-native-community/netinfo": "^11.3.1",
2323
"expo": "~54.0.32",
2424
"expo-camera": "~17.0.10",
25+
"expo-clipboard": "~8.0.8",
2526
"expo-constants": "^18.0.13",
27+
"expo-file-system": "~19.0.22",
2628
"expo-font": "~14.0.11",
2729
"expo-haptics": "^15.0.8",
2830
"expo-image-picker": "~17.0.10",
2931
"expo-linking": "^8.0.11",
3032
"expo-router": "^6.0.22",
33+
"expo-sharing": "~14.0.8",
3134
"expo-status-bar": "~3.0.9",
3235
"i18next": "^23.7.6",
3336
"react": "19.1.0",
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import React, { useState } from "react";
2+
import {
3+
View,
4+
Text,
5+
StyleSheet,
6+
TouchableOpacity,
7+
Modal,
8+
ScrollView,
9+
TextInput,
10+
Platform,
11+
} from "react-native";
12+
import { Ionicons } from "@expo/vector-icons";
13+
import { COLORS } from "../constants/colors";
14+
import { TransactionFilters } from "../types/transaction";
15+
16+
interface Props {
17+
visible: boolean;
18+
filters: TransactionFilters;
19+
onApply: (f: TransactionFilters) => void;
20+
onClose: () => void;
21+
}
22+
23+
const TYPE_OPTIONS: { label: string; value: TransactionFilters["type"] }[] = [
24+
{ label: "All", value: "all" },
25+
{ label: "Sent", value: "sent" },
26+
{ label: "Received", value: "received" },
27+
];
28+
29+
const STATUS_OPTIONS: { label: string; value: TransactionFilters["status"] }[] =
30+
[
31+
{ label: "All", value: "all" },
32+
{ label: "Completed", value: "completed" },
33+
{ label: "Pending", value: "pending" },
34+
{ label: "Failed", value: "failed" },
35+
];
36+
37+
function ChipGroup<T extends string>({
38+
options,
39+
value,
40+
onChange,
41+
}: {
42+
options: { label: string; value: T }[];
43+
value: T;
44+
onChange: (v: T) => void;
45+
}) {
46+
return (
47+
<View style={chipStyles.row}>
48+
{options.map((o) => (
49+
<TouchableOpacity
50+
key={o.value}
51+
style={[chipStyles.chip, value === o.value && chipStyles.chipActive]}
52+
onPress={() => onChange(o.value)}
53+
activeOpacity={0.8}
54+
>
55+
<Text
56+
style={[
57+
chipStyles.chipText,
58+
value === o.value && chipStyles.chipTextActive,
59+
]}
60+
>
61+
{o.label}
62+
</Text>
63+
</TouchableOpacity>
64+
))}
65+
</View>
66+
);
67+
}
68+
69+
const chipStyles = StyleSheet.create({
70+
row: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
71+
chip: {
72+
paddingHorizontal: 16,
73+
paddingVertical: 8,
74+
borderRadius: 100,
75+
borderWidth: 1.5,
76+
borderColor: "#E0E0E0",
77+
},
78+
chipActive: { borderColor: COLORS.primary, backgroundColor: "#F0FDF4" },
79+
chipText: { fontSize: 14, fontFamily: "Outfit_500Medium", color: "#666" },
80+
chipTextActive: { color: COLORS.primary },
81+
});
82+
83+
export function TransactionFilterSheet({
84+
visible,
85+
filters,
86+
onApply,
87+
onClose,
88+
}: Props) {
89+
const [local, setLocal] = useState<TransactionFilters>(filters);
90+
91+
const update = <K extends keyof TransactionFilters>(
92+
key: K,
93+
value: TransactionFilters[K]
94+
) => setLocal((prev) => ({ ...prev, [key]: value }));
95+
96+
const reset = () => setLocal({ type: "all", status: "all", search: "" });
97+
98+
const apply = () => {
99+
onApply(local);
100+
onClose();
101+
};
102+
103+
// Sync when opened
104+
React.useEffect(() => {
105+
if (visible) setLocal(filters);
106+
}, [visible, filters]);
107+
108+
return (
109+
<Modal
110+
visible={visible}
111+
animationType="slide"
112+
transparent
113+
onRequestClose={onClose}
114+
>
115+
<View style={styles.backdrop}>
116+
<TouchableOpacity style={styles.backdropTouch} onPress={onClose} />
117+
<View style={styles.sheet}>
118+
{/* Handle */}
119+
<View style={styles.handle} />
120+
121+
{/* Title row */}
122+
<View style={styles.titleRow}>
123+
<Text style={styles.title}>Filter Transactions</Text>
124+
<TouchableOpacity onPress={reset}>
125+
<Text style={styles.resetText}>Reset</Text>
126+
</TouchableOpacity>
127+
</View>
128+
129+
<ScrollView
130+
showsVerticalScrollIndicator={false}
131+
contentContainerStyle={styles.body}
132+
>
133+
{/* Type */}
134+
<Text style={styles.sectionLabel}>Type</Text>
135+
<ChipGroup
136+
options={TYPE_OPTIONS}
137+
value={local.type}
138+
onChange={(v) => update("type", v)}
139+
/>
140+
141+
{/* Status */}
142+
<Text style={styles.sectionLabel}>Status</Text>
143+
<ChipGroup
144+
options={STATUS_OPTIONS}
145+
value={local.status}
146+
onChange={(v) => update("status", v)}
147+
/>
148+
149+
{/* Date range */}
150+
<Text style={styles.sectionLabel}>Date From</Text>
151+
<TextInput
152+
style={styles.input}
153+
placeholder="YYYY-MM-DD"
154+
placeholderTextColor="#bbb"
155+
value={local.dateFrom ?? ""}
156+
onChangeText={(v) => update("dateFrom", v || undefined)}
157+
keyboardType="numbers-and-punctuation"
158+
/>
159+
160+
<Text style={styles.sectionLabel}>Date To</Text>
161+
<TextInput
162+
style={styles.input}
163+
placeholder="YYYY-MM-DD"
164+
placeholderTextColor="#bbb"
165+
value={local.dateTo ?? ""}
166+
onChangeText={(v) => update("dateTo", v || undefined)}
167+
keyboardType="numbers-and-punctuation"
168+
/>
169+
170+
{/* Amount range */}
171+
<Text style={styles.sectionLabel}>Amount Range</Text>
172+
<View style={styles.amountRow}>
173+
<TextInput
174+
style={[styles.input, styles.amountInput]}
175+
placeholder="Min"
176+
placeholderTextColor="#bbb"
177+
value={local.amountMin ?? ""}
178+
onChangeText={(v) => update("amountMin", v || undefined)}
179+
keyboardType="decimal-pad"
180+
/>
181+
<Text style={styles.amountSep}></Text>
182+
<TextInput
183+
style={[styles.input, styles.amountInput]}
184+
placeholder="Max"
185+
placeholderTextColor="#bbb"
186+
value={local.amountMax ?? ""}
187+
onChangeText={(v) => update("amountMax", v || undefined)}
188+
keyboardType="decimal-pad"
189+
/>
190+
</View>
191+
</ScrollView>
192+
193+
{/* Apply */}
194+
<View style={styles.footer}>
195+
<TouchableOpacity
196+
style={styles.applyBtn}
197+
onPress={apply}
198+
activeOpacity={0.8}
199+
>
200+
<Ionicons
201+
name="checkmark"
202+
size={18}
203+
color={COLORS.secondary}
204+
style={{ marginRight: 6 }}
205+
/>
206+
<Text style={styles.applyBtnText}>Apply Filters</Text>
207+
</TouchableOpacity>
208+
</View>
209+
</View>
210+
</View>
211+
</Modal>
212+
);
213+
}
214+
215+
const styles = StyleSheet.create({
216+
backdrop: { flex: 1, justifyContent: "flex-end" },
217+
backdropTouch: {
218+
...StyleSheet.absoluteFillObject,
219+
backgroundColor: "rgba(0,0,0,0.4)",
220+
},
221+
sheet: {
222+
backgroundColor: COLORS.white,
223+
borderTopLeftRadius: 24,
224+
borderTopRightRadius: 24,
225+
paddingBottom: Platform.OS === "ios" ? 40 : 24,
226+
maxHeight: "85%",
227+
},
228+
handle: {
229+
width: 40,
230+
height: 4,
231+
borderRadius: 2,
232+
backgroundColor: "#E0E0E0",
233+
alignSelf: "center",
234+
marginTop: 12,
235+
marginBottom: 8,
236+
},
237+
titleRow: {
238+
flexDirection: "row",
239+
justifyContent: "space-between",
240+
alignItems: "center",
241+
paddingHorizontal: 20,
242+
paddingVertical: 12,
243+
borderBottomWidth: 1,
244+
borderBottomColor: "#F0F0F0",
245+
},
246+
title: {
247+
fontSize: 18,
248+
fontFamily: "Outfit_700Bold",
249+
color: COLORS.black,
250+
},
251+
resetText: {
252+
fontSize: 14,
253+
fontFamily: "Outfit_500Medium",
254+
color: "#EF4444",
255+
},
256+
body: { paddingHorizontal: 20, paddingTop: 16, gap: 8, paddingBottom: 8 },
257+
sectionLabel: {
258+
fontSize: 13,
259+
fontFamily: "Outfit_600SemiBold",
260+
color: "#999",
261+
textTransform: "uppercase",
262+
letterSpacing: 0.5,
263+
marginTop: 12,
264+
marginBottom: 6,
265+
},
266+
input: {
267+
borderWidth: 1.5,
268+
borderColor: "#E0E0E0",
269+
borderRadius: 12,
270+
paddingHorizontal: 14,
271+
paddingVertical: 10,
272+
fontSize: 14,
273+
fontFamily: "Outfit_400Regular",
274+
color: COLORS.black,
275+
},
276+
amountRow: { flexDirection: "row", alignItems: "center", gap: 8 },
277+
amountInput: { flex: 1 },
278+
amountSep: {
279+
fontSize: 16,
280+
color: "#999",
281+
fontFamily: "Outfit_400Regular",
282+
},
283+
footer: { paddingHorizontal: 20, paddingTop: 16 },
284+
applyBtn: {
285+
backgroundColor: COLORS.primary,
286+
borderRadius: 100,
287+
paddingVertical: 16,
288+
flexDirection: "row",
289+
alignItems: "center",
290+
justifyContent: "center",
291+
},
292+
applyBtnText: {
293+
fontSize: 16,
294+
fontFamily: "Outfit_600SemiBold",
295+
color: COLORS.secondary,
296+
},
297+
});

0 commit comments

Comments
 (0)