diff --git a/app/screens/CreditBalanceScreen.tsx b/app/screens/CreditBalanceScreen.tsx
index 6e96b01c..02bc3145 100644
--- a/app/screens/CreditBalanceScreen.tsx
+++ b/app/screens/CreditBalanceScreen.tsx
@@ -1,61 +1,305 @@
-import React, { useState } from 'react';
-import { View, Text, TextInput, Button, FlatList, StyleSheet } from 'react-native';
-import { useCreditStore, CreditTransaction } from '../stores/creditStore';
+import React, { useMemo, useState } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ StyleSheet,
+ FlatList,
+ ScrollView,
+ Pressable,
+ Alert,
+} from 'react-native';
+import { useCreditStore, type AccountCredit, type CreditLot } from '../stores/creditStore';
+
+type Tab = 'balance' | 'analytics' | 'history' | 'wallets';
+
+const colorScheme = {
+ bg: '#f7fafc',
+ card: '#ffffff',
+ border: '#e2e8f0',
+ primary: '#1d4ed8',
+ credit: '#15803d',
+ debit: '#b91c1c',
+ warning: '#c2410c',
+ text: '#0f172a',
+ textMuted: '#475569',
+};
+
+const currency = (n: number): string => n.toLocaleString(undefined, { minimumFractionDigits: 0 });
const styles = StyleSheet.create({
- container: { flex: 1, padding: 16 },
- header: { fontSize: 18, fontWeight: '700', marginVertical: 8 },
- balance: { fontSize: 32, fontWeight: '800', color: '#15803d' },
- label: { fontSize: 12, color: '#666', marginTop: 8 },
- input: { height: 40, borderColor: '#ccc', borderWidth: 1, paddingHorizontal: 8, borderRadius: 4, marginTop: 4 },
- row: { flexDirection: 'row', gap: 8, marginTop: 8 },
- item: { paddingVertical: 6, borderBottomWidth: 1, borderColor: '#eee' },
- credit: { color: '#15803d' },
- debit: { color: '#b91c1c' },
+ container: { flex: 1, backgroundColor: colorScheme.bg },
+ scrollContent: { padding: 16, gap: 12 },
+ heroCard: {
+ backgroundColor: colorScheme.card,
+ borderRadius: 12,
+ padding: 20,
+ borderWidth: 1,
+ borderColor: colorScheme.border,
+ },
+ heroLabel: { color: colorScheme.textMuted, fontSize: 12, textTransform: 'uppercase', letterSpacing: 1 },
+ heroBalance: { color: colorScheme.text, fontSize: 36, fontWeight: '800', marginTop: 4 },
+ heroSub: { color: colorScheme.textMuted, fontSize: 13, marginTop: 6 },
+ tabRow: { flexDirection: 'row', gap: 8, marginBottom: 8 },
+ tab: {
+ flex: 1,
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ backgroundColor: colorScheme.card,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: colorScheme.border,
+ alignItems: 'center',
+ },
+ tabActive: { backgroundColor: colorScheme.primary, borderColor: colorScheme.primary },
+ tabLabel: { color: colorScheme.text, fontWeight: '600' },
+ tabLabelActive: { color: '#fff' },
+ card: {
+ backgroundColor: colorScheme.card,
+ borderRadius: 12,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: colorScheme.border,
+ gap: 12,
+ },
+ cardTitle: { fontWeight: '700', fontSize: 16, color: colorScheme.text },
+ cardSubtitle: { color: colorScheme.textMuted, fontSize: 12 },
+ row: { flexDirection: 'row', gap: 8 },
+ input: {
+ flex: 1,
+ borderColor: colorScheme.border,
+ borderWidth: 1,
+ borderRadius: 8,
+ paddingHorizontal: 10,
+ paddingVertical: 8,
+ color: colorScheme.text,
+ backgroundColor: '#fff',
+ },
+ button: {
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ backgroundColor: colorScheme.primary,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ buttonLabel: { color: '#fff', fontWeight: '600' },
+ buttonGhost: {
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ backgroundColor: '#fff',
+ borderWidth: 1,
+ borderColor: colorScheme.border,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ buttonGhostLabel: { color: colorScheme.text, fontWeight: '600' },
+ itemRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: 8,
+ borderBottomWidth: 1,
+ borderColor: colorScheme.border,
+ },
+ itemAmount: { fontWeight: '600' },
+ credit: { color: colorScheme.credit },
+ debit: { color: colorScheme.debit },
+ muted: { color: colorScheme.textMuted },
+ expiryPill: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 12,
+ alignSelf: 'flex-start',
+ },
+ statGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
+ statTile: {
+ flexBasis: '48%',
+ backgroundColor: '#fff',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: colorScheme.border,
+ padding: 12,
+ },
+ statLabel: { color: colorScheme.textMuted, fontSize: 12 },
+ statValue: { color: colorScheme.text, fontSize: 18, fontWeight: '700', marginTop: 2 },
+ chartBar: {
+ height: 8,
+ backgroundColor: '#e2e8f0',
+ borderRadius: 4,
+ overflow: 'hidden',
+ },
+ chartFill: {
+ height: '100%',
+ backgroundColor: colorScheme.primary,
+ },
});
-const sign = (tx: CreditTransaction) => (tx.amount >= 0 ? styles.credit : styles.debit);
+const BucketBar: React.FC<{ label: string; value: number; total: number; tone?: 'pos' | 'neg' | 'mute' }> = ({
+ label,
+ value,
+ total,
+ tone = 'pos',
+}) => {
+ const pct = total > 0 ? Math.min(100, Math.round((value / total) * 100)) : 0;
+ const fill =
+ tone === 'neg' ? colorScheme.debit : tone === 'mute' ? colorScheme.textMuted : colorScheme.primary;
+ return (
+
+
+ {label}
+ {currency(value)}
+
+
+
+
+
+ );
+};
-export const CreditBalanceScreen: React.FC<{ subscriber?: string }> = ({ subscriber = 'me' }) => {
- const { issueCredit, applyCredit, expireCredits, getBalance, getAccount } = useCreditStore();
- const [amount, setAmount] = useState('100');
- const [due, setDue] = useState('50');
- // Read from store on each render so issuing/applying refreshes the figures.
- const balance = useCreditStore((s) => s.accounts[subscriber]?.balance ?? 0);
- void balance; // stored balance; available balance shown below accounts for expiry
+const AnalyticsPanel: React.FC<{ account: AccountCredit }> = ({ account }) => {
+ const issued = useMemo(
+ () =>
+ account.transactions
+ .filter((t) => t.kind === 'issue')
+ .reduce((s, t) => s + t.amount, 0),
+ [account.transactions]
+ );
+ const applied = useMemo(
+ () =>
+ account.transactions
+ .filter((t) => t.kind === 'apply')
+ .reduce((s, t) => s + Math.abs(t.amount), 0),
+ [account.transactions]
+ );
+ const transferIn = useMemo(
+ () =>
+ account.transactions
+ .filter((t) => t.kind === 'transfer_in')
+ .reduce((s, t) => s + t.amount, 0),
+ [account.transactions]
+ );
+ const transferOut = useMemo(
+ () =>
+ account.transactions
+ .filter((t) => t.kind === 'transfer_out')
+ .reduce((s, t) => s + Math.abs(t.amount), 0),
+ [account.transactions]
+ );
+ const expired = useMemo(
+ () =>
+ account.transactions
+ .filter((t) => t.kind === 'expire')
+ .reduce((s, t) => s + Math.abs(t.amount), 0),
+ [account.transactions]
+ );
- const available = getBalance(subscriber);
- const account = getAccount(subscriber);
+ const consumptionRate = issued > 0 ? Math.round((applied / issued) * 100) : 0;
+ const channelTotal = issued + transferIn;
+
+ const expiryBuckets = useMemo(() => {
+ const nowSec = Math.floor(Date.now() / 1000);
+ const buckets = { within_7d: 0, within_30d: 0, beyond_30d: 0, no_expiry: 0 };
+ for (const lot of account.lots) {
+ if (lot.remaining <= 0) continue;
+ if (!lot.expiresAt) {
+ buckets.no_expiry += lot.remaining;
+ continue;
+ }
+ const secToExpiry = lot.expiresAt - nowSec;
+ if (secToExpiry <= 7 * 86_400) buckets.within_7d += lot.remaining;
+ else if (secToExpiry <= 30 * 86_400) buckets.within_30d += lot.remaining;
+ else buckets.beyond_30d += lot.remaining;
+ }
+ return buckets;
+ }, [account.lots]);
return (
-
- Account Credit
- {available.toLocaleString()}
- Available credit (excludes expired)
-
- Issue credit
-
-