Untrack internal planning docs

LEDGA_REDESIGN.md and PROJECT_PLAN.md are kept locally for reference
but shouldn't live in the public repo.
This commit is contained in:
ngenohkevin
2026-05-16 13:26:22 +03:00
parent 51264301a8
commit 85c3f167ab
3 changed files with 4 additions and 1713 deletions
+4
View File
@@ -17,3 +17,7 @@ build/
*.class *.class
.kotlin/ .kotlin/
.claude/ .claude/
# Internal planning docs — kept locally, not pushed.
LEDGA_REDESIGN.md
PROJECT_PLAN.md
-943
View File
@@ -1,943 +0,0 @@
# Ledga — Major Redesign Spec (v2)
> Target visual direction: **Modern Fintech Bento**. Soft neutral surfaces, M-Pesa green as a single accent, big confident numbers, generous whitespace, rounded everything. Feels at home next to Revolut / Cash App / Monzo while staying friendly for a Kenyan family audience.
>
> Scope: full UI overhaul + four new feature areas:
> 1. **Insights** — smart, derived suggestions (recurring detection, fee tips, anomaly callouts)
> 2. **Goals** — savings targets with progress + ETA
> 3. **Multi-SIM** — switch between two M-Pesa lines on a dual-SIM device
> 4. **In-app update flow** — surface release notes + one-tap install when a new APK is published
---
## 1. Design Tokens
These are the canonical names. The Compose `Color.kt` / `Theme.kt` files should mirror them 1:1. (They're also already saved into the pencil document via `set_variables`, so when Pencil starts cooperating we can wire them up as variables there too.)
### 1.1 Color — Light
| Token | Hex | Use |
|----------------------|-----------|--------------------------------------------------------|
| `--bg` | `#FAFAF7` | Page background (warm off-white, not stark) |
| `--surface` | `#FFFFFF` | Card / sheet background |
| `--surface-2` | `#F2F2EE` | Subtle elevation, chip backgrounds |
| `--ink` | `#0E1311` | Primary text, big numbers |
| `--ink-2` | `#3A4240` | Body text |
| `--muted` | `#6B7270` | Secondary text, captions |
| `--muted-2` | `#A4A8A5` | Disabled, hints |
| `--line` | `#E8E8E2` | Dividers, hairlines |
| `--accent` | `#10A37F` | M-Pesa-ish green, but de-saturated for premium feel |
| `--accent-soft` | `#E2F3EC` | Accent tints (chip backgrounds, progress tracks) |
| `--accent-deep` | `#0E7C5F` | Accent text on light, hover states |
| `--ink-on-accent` | `#FFFFFF` | Text on accent surfaces |
| `--danger` | `#D14B4B` | Destructive, over-budget, errors |
| `--danger-soft` | `#FBE8E8` | Danger backgrounds |
| `--warning` | `#E7A82C` | "Budget at 80%", fuliza outstanding |
| `--warning-soft` | `#FBEFD2` | Warning backgrounds |
| `--inflow` | `#1F8B5A` | Received / deposit / refund amounts |
| `--outflow` | `#0E1311` | Outflow uses `--ink` (not red) — red is for alerts only|
### 1.2 Color — Dark
Same token names, different values. Designed for OLED-friendly true-blacks while keeping the accent vivid.
| Token | Hex |
|----------------------|-----------|
| `--bg` | `#0A0C0B` |
| `--surface` | `#15191B` |
| `--surface-2` | `#1E2326` |
| `--ink` | `#F4F6F5` |
| `--ink-2` | `#C9CFCC` |
| `--muted` | `#8E948F` |
| `--muted-2` | `#5A615D` |
| `--line` | `#262B2D` |
| `--accent` | `#3CD9A8` | brighter on dark for pop |
| `--accent-soft` | `#0F2A23` |
| `--accent-deep` | `#85F2C8` |
| `--ink-on-accent` | `#06120E` |
| `--danger` | `#FF7A7A` |
| `--danger-soft` | `#2A1414` |
| `--warning` | `#FFC25E` |
| `--warning-soft` | `#2A2009` |
| `--inflow` | `#3CD9A8` |
| `--outflow` | `#F4F6F5` |
### 1.3 Category palette (same on both themes — saturated enough to stay distinct)
```
Groceries #4CAF50 Transport #3478F6 Bills #FF9F2D
Airtime/Data #A155F0 Food #EC4072 Send Money #6E7B8B
Received #06B6D4 Withdrawal #8B5A3C Deposit #7CC242
Shopping #FF6A3D International #4F58D9 Savings/Loans #089B91
Other #9E9E9E
```
Always paired with a 12% tint as the icon-tile background, full color for the icon glyph itself.
### 1.4 Typography
Font: **Inter** (variable) everywhere. Numbers use `font-feature-settings: "tnum"` (tabular figures) so balances align in columns.
| Style | Size | Weight | Line | Letter-spacing | Use |
|----------------|------|--------|------|-----------------|--------------------------------------------------|
| Display XL | 56 | 700 | 1.05 | -1.5 | Onboarding hero amounts (rare) |
| Display L | 44 | 700 | 1.05 | -1.0 | Home "Spent this month" |
| Display M | 32 | 700 | 1.1 | -0.5 | Goal current amount, trend hero |
| Title L | 24 | 700 | 1.2 | -0.2 | Screen titles (Home, Transactions, …) |
| Title M | 20 | 600 | 1.25 | -0.1 | Section headers, sheet titles |
| Title S | 17 | 600 | 1.3 | 0 | Card titles |
| Body L | 16 | 500 | 1.4 | 0 | Transaction recipient, primary body |
| Body M | 14 | 500 | 1.4 | 0 | Secondary body, descriptions |
| Caption | 12 | 500 | 1.3 | 0.1 | Day labels, meta info |
| Overline | 11 | 600 | 1.3 | 0.5 (uppercase) | Tab labels, section eyebrows |
| Mono S | 13 | 500 | 1.4 | 0 | Transaction codes (use system mono) |
### 1.5 Spacing scale
`4 · 8 · 12 · 16 · 20 · 24 · 32 · 48 · 64`
- Screen horizontal padding: **20**
- Card inner padding: **20** (compact cards 16)
- Section vertical gap: **24**
- Tight group vertical gap: **12**
### 1.6 Radius
`xs:8 · sm:12 · md:16 · lg:24 · xl:32 · pill:999`
- Cards: **24** (lg)
- Sheets: **32 top-only**
- Inputs / chips: **pill**
- Tab bar pill: **36**
- Icon tiles: **14**
### 1.7 Elevation
No drop shadows on light theme — depth comes from **1px `--line` borders + subtle surface-2 wells**. On dark theme, no shadows either — depth via surface stepping (`--surface` over `--bg`).
The only exception: the floating tab bar uses a soft drop shadow on light (y:8, blur:24, color: `#0E1311 @ 6%`).
### 1.8 Motion
| Token | Duration | Easing | Use |
|------------------|----------|-----------------------------------|------------------------------------|
| `motion-fast` | 120ms | `cubic-bezier(0.2, 0, 0, 1)` | Press states, ripples |
| `motion-base` | 240ms | `cubic-bezier(0.2, 0, 0, 1)` | Sheet open, chip select |
| `motion-emph` | 360ms | `cubic-bezier(0.2, 0, 0, 1)` | Screen enter, hero number flip |
| `motion-spring` | spring(stiffness=300, damping=30) | Tab switch, FAB scale |
Numbers (balances, totals) **animate from old → new value** when changed (counting up/down). Spec: 600ms `easeOutCubic`. Use Compose `animateFloatAsState` + `String.format`.
---
## 2. Component Inventory
### 2.1 Atoms
- **Avatar** 40 / 32 / 24 — circular, initial inside on `--surface-2`. Used in account switcher, transaction rows for person-to-person.
- **CategoryIcon tile** 40×40 — 14-radius rounded square, category color @ 12% bg, glyph in full color.
- **Chip / Filter** — pill, 32 height, 12px horizontal padding, `--surface-2` bg, `--ink` text. Selected: `--accent-soft` bg, `--accent-deep` text, 1px `--accent` border.
- **Switch** — Material 3 with `--accent` selected track.
- **TextField** — 56 height, pill radius, `--surface-2` bg, no outline. Floating label collapses on focus.
- **Button** — Primary (filled accent, pill, 52h), Secondary (1px `--line`, `--surface`), Ghost (text only), Danger (filled `--danger`).
- **Badge** — 20h pill, 8px horizontal, caption text. Variants: neutral, success, warning, danger.
- **ProgressBar** — 8h pill, `--surface-2` track, `--accent` fill. Over-budget: `--danger` fill.
- **ProgressRing** — 56 / 80 / 120 sizes, 6 stroke, same color rules as ProgressBar.
### 2.2 Molecules
- **TransactionRow**
```
┌────────────────────────────────────────────────┐
│ [🛒] Naivas Supermarket -Ksh 1,200 │
│ Groceries · 2:15 PM Bal 3,500 │
└────────────────────────────────────────────────┘
```
Icon (40), middle stack (recipient L, meta M muted), right stack (amount L, balance Caption muted). 16px vertical padding, no borders between rows — separated only by 12px gap inside a card.
- **StatCard** — title (Overline muted), value (Display M ink), delta chip (Caption with ▲/▼).
- **BentoTile** — generic 24-radius card with optional header row (icon + title + trailing chevron) and content slot.
- **DayHeader** — sticky inside the transactions list. "Today · Ksh 3,420" left, weekday caption right.
- **AccountChip** — used in Home header for multi-SIM. Avatar + carrier + chevron, opens AccountSwitcherSheet.
- **InsightCard** — illustrated, 24-radius, accent-soft bg, icon top-left, title + body, optional CTA.
- **GoalCard** — ProgressRing left, name + amount stack right, ETA badge bottom.
### 2.3 Organisms
- **TopBar** — sticky on every screen. Three slots: leading (Avatar + greeting OR back button), title (only on detail screens), trailing (icons: notifications, settings).
- **PillTabBar** — floating, bottom-anchored, 4 tabs: Home / Activity / Insights / You.
- Spec from mobile-app guideline: container padding 12 top / 21 sides+bottom, pill 62h with 36 radius, items vertical (18 icon + 10 uppercase label), active = `--accent` fill + `--ink-on-accent`.
- Slots: Home, Activity (combines Transactions + Trends in one screen with segmented top), Insights, You (profile/settings/goals).
- FAB-like + center action removed to keep the bar honest — actions belong inside screens.
- **BottomSheet** — radius 32 top-only, 4-wide 40-long grab handle, 24px padding, max 90% screen height.
- **EmptyState** — 240 illustrated SVG centered, Title M, Body M muted, single primary CTA.
---
## 3. Information Architecture
Old IA: Home / Transactions / Trends / Settings (4 tabs).
New IA: **Home / Activity / Insights / You** (4 tabs) — but with more inside.
```
┌─ Home — overview, hero spend, quick stats, recent
│
├─ Activity — segmented [ Transactions · Trends ]
│ ├─ Transactions — day-grouped list, search, filter chips
│ └─ Trends — charts, top merchants, fees, heatmap
│
├─ Insights — smart cards (auto-generated)
│ • recurring detection
│ • fee tips
│ • spending anomalies
│ • category trends
│
└─ You — profile + grouped settings + goals
├─ Accounts — multi-SIM management
├─ Goals — list + add
├─ Budgets
├─ Appearance
├─ Notifications
├─ Data
├─ Backup
└─ About / Updates
```
**Why this IA**:
- "Activity" merges Transactions + Trends because users always toggle between "what happened" and "what's the pattern" — they shouldn't be two tabs apart.
- "You" pulls in Goals because goals are personal, not screen-scoped. It also hosts Accounts (multi-SIM).
- "Insights" earns top-level because the feature only works if users see it without going looking.
---
## 4. Screen Specs
For each screen below: **status bar 62**, then **wrapper** with horizontal padding `--pad-screen` (20) and gap `--gap-7` (24) between sections, then **pill tab bar** floating at the bottom (where shown).
Bottom padding on the scroll content: **120** (tab bar height 62 + 21 padding + 32 breathing room) so the last item is never hidden behind the bar.
### 4.1 Onboarding (4 steps, swipeable + dotted progress)
**Step 1 — Welcome**
```
┌──────────────────────────────────────────────┐
│ │
│ │
│ ● ○ ○ ○ │
│ │
│ ┌──────┐ │
│ │ L │ (logo, 88px) │
│ └──────┘ │
│ │
│ Track every M-Pesa shilling. │
│ Automatically. │
│ │
│ Ledga reads your M-Pesa SMS and turns │
│ it into a calm, private spending │
│ picture. Nothing leaves your phone. │
│ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Get started → │ │
│ └────────────────────────────────────┘ │
│ │
│ I'll set this up later │
└──────────────────────────────────────────────┘
```
- Hero text: Title L
- Body: Body L muted, max-width ~320
- Primary CTA: 52h pill filled accent
- "Set up later" = Ghost button, only proceeds with read-only mode
**Step 2 — SMS permission (the trust moment)**
```
┌──────────────────────────────────────────────┐
│ ● ● ○ ○ │
│ │
│ ┌──────┐ │
│ │ 📩 │ (icon tile, 56) │
│ └──────┘ │
│ │
│ Let Ledga read M-Pesa SMS │
│ │
│ • Only messages from MPESA are read │
│ • Nothing is sent to a server │
│ • You can revoke this in system settings │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 🔒 Your data is on this device only │ │
│ │ and is backed up to your Google │ │
│ │ Drive if you turn it on. │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Allow SMS access → │ │
│ └────────────────────────────────────┘ │
│ │
│ Not now │
└──────────────────────────────────────────────┘
```
- The accent-soft callout card under the bullets is the "trust signal" — keep it.
- "Not now" routes to a minimal manual-entry mode (out of scope this release, just a stub screen).
**Step 3 — Import history**
```
┌──────────────────────────────────────────────┐
│ ● ● ● ○ │
│ │
│ Bring in your history? │
│ │
│ We can scan the M-Pesa messages already │
│ on your phone to build a full picture. │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 📊 342 M-Pesa messages found │ │
│ │ Going back to Aug 2024 │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Import all 342 messages → │ │
│ └────────────────────────────────────┘ │
│ │
│ Start fresh │
│ │
│ ───── while importing ───── │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Importing… │ │
│ │ ▓▓▓▓▓▓▓▓▓░░░░░░ 68% │ │
│ │ 231 of 342 messages │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
- Show found-count BEFORE asking, so the user can decide informedly.
- Progress card replaces the CTA when import is running, then auto-advances.
**Step 4 — Multi-SIM detection (NEW, optional)**
Only shown if two active M-Pesa lines are detected via subscription IDs in the SMS data.
```
┌──────────────────────────────────────────────┐
│ ● ● ● ● │
│ │
│ We spotted two M-Pesa lines │
│ │
│ You can name them so it's clear which is │
│ which. Both are tracked separately and you │
│ can switch from the home screen anytime. │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ⓅⓛⓁ Line 1 Safaricom · 07XX…23 │ │
│ │ Name ┌──────────────────────────┐ │ │
│ │ │ Personal × │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ⓆⓀ Line 2 Safaricom · 07XX…87 │ │
│ │ Name ┌──────────────────────────┐ │ │
│ │ │ Business × │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ You're set → │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
### 4.2 Home
```
┌──────────────────────────────────────────────┐
│ Status bar │
├──────────────────────────────────────────────┤
│ │
│ ⒶPersonal ▾ 🔔 ⚙ │ ← AccountChip + 2 icon buttons
│ │
│ Hi, Kevin │ Title L
│ Here's how March is going. │ Body M muted
│ │
│ ┌──────────────────────────────────────────┐ │ ← HERO bento card
│ │ SPENT THIS MONTH › │ │ overline muted
│ │ │ │
│ │ Ksh 45,200 │ │ Display L
│ │ ▼ 12% vs February · Ksh 320 in fees │ │ Body M w/ inline chip
│ │ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓░░░ 78% of 60,000 budget │ │ progress
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │ ← 2-up small bentos
│ │ BALANCE │ │ TOP CATEGORY │ │
│ │ Ksh 12,500 │ │ 🛒 Groceries │ │
│ │ from last tx │ │ Ksh 18,200 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Donut bento
│ │ Where it went ›│ │
│ │ │ │
│ │ ○─◐─● Groceries 40%│ │
│ │ ╱ donut ╲ Transport 18│ │
│ │ │ 45,200 │ Bills 14│ │
│ │ ╲ ╱ Food 11│ │
│ │ ───── Other 17│ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Insights teaser
│ │ ✨ INSIGHTS ›│ │
│ │ │ │
│ │ You're spending 40% more on transport │ │
│ │ this week. 3 Uber trips, all weekends. │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Recent activity See all│ ← section header
│ ┌──────────────────────────────────────────┐ │
│ │ [🛒] Naivas −Ksh 1,200 │ │
│ │ 2:15 PM Bal 3,500 │ │
│ │ ───────────────────────────────────── │ │ hairline
│ │ [📱] Airtime −Ksh 100 │ │
│ │ 12:00 PM │ │
│ │ ───────────────────────────────────── │ │
│ │ [💰] Jane Doe +Ksh 5,000 │ │ inflow color
│ │ 11:00 AM │ │
│ └──────────────────────────────────────────┘ │
│ │
│ (UPDATE BANNER, conditional, see §4.10) │
│ │
│ ⌒ ⌒ ⌒ ⌒ pill tab bar floating │
└──────────────────────────────────────────────┘
```
Behaviors:
- The hero `Spent` number animates from last value to new on receipt of a new SMS.
- Tapping the hero card → Trends (Activity tab → Trends segment).
- Tapping "Balance" small card → reveals "balance history" sparkline in-place (300ms expand, doesn't navigate).
- Tapping "Top category" → filter Transactions list by that category.
- Donut card uses Vico segments; legend is right-justified.
- Insights teaser shows the highest-priority unresolved insight; rotates daily.
### 4.3 Activity — Transactions segment
```
┌──────────────────────────────────────────────┐
│ ‹ │
│ Activity │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ [ Transactions ] Trends │ │ ← segmented (pill)
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 🔍 Search merchants, codes… │ │ ← search input
│ └──────────────────────────────────────────┘ │
│ │
│ All · Sent · Received · Bills · Goods … ‹ ›│ ← horizontal chip row
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ TODAY · Out 3,420 · In 5,000 │ │ ← sticky day header
│ │ │ │
│ │ [🛒] Naivas −Ksh 1,200 │ │
│ │ [📱] Airtime −Ksh 100 │ │
│ │ [💰] Jane Doe +Ksh 5,000 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ YESTERDAY · Out 2,150 │ │
│ │ [🍔] KFC Westgate −Ksh 850 │ │
│ │ [🚗] Uber −Ksh 1,300 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ WED 18 MAR │ │
│ │ [⚡] KPLC −Ksh 2,500 │ │
│ │ Account 12345678 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ pill tab bar │
└──────────────────────────────────────────────┘
```
Interactions:
- Tap row → TransactionDetailSheet (§4.9).
- Long-press row → quick category picker (mini sheet).
- Filter chips multi-select; the row scrolls horizontally; "Customize…" at end opens the full filter sheet.
- Search ranks by transaction code prefix > merchant name fuzzy > recipient phone.
### 4.4 Activity — Trends segment
```
┌──────────────────────────────────────────────┐
│ Activity │
│ │
│ [ Transactions ] [ Trends ] │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 7D [ 30D ] 90D 1Y Custom │ │ ← period selector pill
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Hero compare bento
│ │ NET CASHFLOW │ │
│ │ │ │
│ │ −Ksh 39,880 ▼ 8% vs prev 30D │ │
│ │ Out 45,200 In 5,320 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Daily bars
│ │ Daily spending │ │
│ │ │ │
│ │ ▁▂█▃▅▂▁▄▆▂▁▃▅▇▂▁▃▆▄▂▃▅▇▃▁▂▄▆▂▁ │ │
│ │ 1 5 10 15 20 25 30 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Heatmap calendar (NEW)
│ │ Spending heatmap › │ │
│ │ │ │
│ │ M T W T F S S │ │
│ │ □ □ ▤ ▤ ▥ ▦ ■ │ │
│ │ ▤ ■ ▦ ▤ ■ ▦ ▥ │ │ 4 intensity steps
│ │ ▦ ▤ ▥ ■ ▤ □ ▦ │ │
│ │ ▤ ▥ ▦ ▤ ▤ ■ ▥ │ │
│ │ ▦ ▤ □ ▥ □ ▤ ▦ │ │
│ │ low ▢▢▢▢ high │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Top merchants
│ │ Top merchants › │ │
│ │ 🛒 Naivas 5 visits Ksh 8,400 │ │
│ │ 🚗 Uber 4 trips Ksh 5,200 │ │
│ │ 🍔 KFC 3 visits Ksh 2,550 │ │
│ │ ⚡ KPLC 1 bill Ksh 2,500 │ │
│ │ 📱 Airtime 9 buys Ksh 900 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Fees summary (NEW)
│ │ 💸 FEES PAID │ │
│ │ Ksh 320 over 30 days │ │
│ │ ▲ 12% vs previous 30D │ │
│ │ Most fees came from Withdraw (Ksh 215) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
The heatmap is the high-value new visual — turns "you spent more last Friday" into something instantly readable. 4 intensity steps using `--accent` opacity 8/24/56/88%.
### 4.5 Insights (NEW)
Algorithmically generated. Each card has a "Got it" button (dismiss for 30 days) and optional secondary action (e.g., "See transactions").
```
┌──────────────────────────────────────────────┐
│ Insights │
│ 4 new this week │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Anomaly insight
│ │ 🚨 WATCH OUT │ │
│ │ You spent 40% more on transport this │ │
│ │ week than your typical week. │ │
│ │ 3 Uber trips on weekends · Ksh 4,800 │ │
│ │ [ See transactions ] [ Got it ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Recurring detection
│ │ 🔁 RECURRING DETECTED │ │
│ │ KPLC seems to be a monthly bill. │ │
│ │ Avg Ksh 2,420 · last 3 months │ │
│ │ [ Add to budget ] [ Got it ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Fee tip
│ │ 💡 FEE TIP │ │
│ │ You paid Ksh 215 in agent withdrawal │ │
│ │ fees this month. Withdrawing fewer, │ │
│ │ larger amounts could save ~Ksh 90. │ │
│ │ [ Show me how ] [ Got it ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Positive nudge
│ │ ✨ NICE │ │
│ │ Groceries spending is down 14% this │ │
│ │ month. Keep it up! │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Fuliza warning
│ │ ⚠ FULIZA │ │
│ │ Outstanding Fuliza: Ksh 500 │ │
│ │ Last borrowed 3 days ago. │ │
│ │ [ How Fuliza fees work ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Want fewer of these? Settings → Insights │
│ │
└──────────────────────────────────────────────┘
```
Card variants (background colors):
- **Watch out / anomaly**: warning-soft bg, warning icon
- **Recurring**: surface bg, accent icon
- **Fee tip**: accent-soft bg
- **Positive nudge**: accent-soft bg, sparkles icon
- **Fuliza / danger**: danger-soft bg, danger icon
Generation cadence: re-evaluate on every new transaction (cheap rules) + weekly batch (heavier comparisons). All logic local, deterministic — no ML.
### 4.6 Goals (NEW, lives under "You")
```
┌──────────────────────────────────────────────┐
│ ‹ You │
│ Goals + │
│ │
│ ┌──────────────────────────────────────────┐ │ ← Active goal card
│ │ ◖◗ School fees │ │
│ │ 67% Term 2 │ │
│ │ Ksh 40,000 / 60,000 │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ │ │
│ │ ETA 14 Apr · on track ✓ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ◖◗ Pi server budget │ │
│ │ 32% Hardware fund │ │
│ │ Ksh 8,000 / 25,000 │ │
│ │ ▓▓▓▓▓▓░░░░░░░░░░░░░░ │ │
│ │ ETA 12 Jul · slightly behind ⚠ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ◖◗ Eldoret trip │ │
│ │ 100% Completed Mar 4 🎉 │ │
│ │ Ksh 15,000 / 15,000 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Suggested by your activity: │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 💡 You've moved Ksh 5,000 to M-Shwari │
│ │ each month for 3 months. Turn that │ │
│ │ into a goal? │ │
│ │ [ Create goal ] │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
**Goal model**:
```kotlin
data class Goal(
val id: Long,
val name: String,
val targetAmount: Double,
val targetDate: LocalDate?, // nullable → no ETA, only progress
val contributionRule: ContributionRule,
val createdAt: Long,
val completedAt: Long?,
)
sealed class ContributionRule {
// count every M-Shwari/KCB deposit as contribution
object AllSavingsDeposits : ContributionRule()
// count outflows to a specific paybill (e.g. school fees paybill)
data class ToPaybill(val paybill: String) : ContributionRule()
// user manually marks a transaction as contributing
object Manual : ContributionRule()
}
```
Progress is derived. ETA = `(targetAmount - currentAmount) / avgMonthlyContribution * 30 days from today`.
**Add Goal sheet** (full screen sheet):
- Goal name
- Target amount
- Target date (optional)
- Contribution rule (radio: All savings · Specific paybill · Manual mark)
- Color picker (one of the category colors)
### 4.7 You
```
┌──────────────────────────────────────────────┐
│ You │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ⒶⒶ Kevin │ │ ← profile card
│ │ 2 M-Pesa lines · 1,432 tx tracked │ │
│ │ [ Manage accounts ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 🎯 Goals 3 ›│ │
│ │ 💰 Budgets 2 ›│ │
│ │ 🏷 Categories ›│ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 🌓 Appearance │ │
│ │ Theme System › │ │
│ │ Font size Medium › │ │
│ │ Accent ●●●●● › │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 🔔 Notifications │ │
│ │ Daily summary 8:00 PM │ │
│ │ Weekly summary Sun, 6 PM │ │
│ │ Budget alerts ● │ │
│ │ Large transaction alert OFF › │ │
│ │ Insights ● │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 🗄 Data │ │
│ │ Import SMS history › │ │
│ │ Re-parse 14 unknown messages › │ │
│ │ Export (CSV / JSON) › │ │
│ │ Clear all data › │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ☁ Backup │ │
│ │ Google Drive ngenoh@… │ │
│ │ Last backup Today 3 AM │ │
│ │ Back up now › │ │
│ │ Restore › │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ℹ About │ │
│ │ Version 1.0.0 │ │
│ │ [ NEW ] v1.1.0 available Update › │ │
│ │ What's new › │ │
│ │ Privacy › │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
Settings rows are **grouped into bento cards** instead of being a flat list — same `--surface` cards, hairlines between rows inside a card, no hairline at edges. Each "group" = one bento.
### 4.8 Account Switcher Sheet (Multi-SIM)
Triggered from the AccountChip on Home.
```
┌──────────────────────────────────────────────┐
│ │
│ ━━━━ │ ← grab handle
│ │
│ Accounts │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ⓅⓛⓁ Personal ● selected │ │ ← accent border + check
│ │ Safaricom · 07XX…23 │ │
│ │ Bal 12,500 · 1,200 tx │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ⓆⓀ Business │ │
│ │ Safaricom · 07XX…87 │ │
│ │ Bal 4,300 · 232 tx │ │
│ └──────────────────────────────────────┘ │
│ │
│ View │
│ ( ) Selected account only │
│ (●) Combined view │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ + Add another line │ │
│ └──────────────────────────────────────┘ │
│ │
│ Rename · Recolor accounts in You → Accounts│
│ │
└──────────────────────────────────────────────┘
```
**Data model addition**:
```kotlin
@Entity(tableName = "mpesa_accounts")
data class MpesaAccount(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val subscriptionId: Int, // Android SubscriptionInfo id
val phoneNumber: String?, // E.164 if available
val displayName: String, // "Personal" / "Business"
val colorHex: String, // accent for tiles
val isPrimary: Boolean,
)
```
`Transaction.accountId: Long?` is added. Migration:
1. ALTER TABLE add column nullable.
2. Existing rows stay null = "default" account.
3. On next SMS receive, populate the `accounts` table from SubscriptionManager and stamp incoming transactions.
### 4.9 Transaction Detail Sheet
```
┌──────────────────────────────────────────────┐
│ ━━━━ │
│ │
│ [🛒] Naivas Supermarket │
│ BUY GOODS │
│ │
│ −Ksh 1,200.00 │ Display L outflow ink
│ cost Ksh 0.00 │ Caption muted
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Code RK31B7X4ZQ │ │
│ │ Date 21 Mar 2026, 2:15PM │ │
│ │ Balance after Ksh 3,500.00 │ │
│ │ Account Personal ⓅⓛⓁ │ │ ← only if multi-SIM
│ └──────────────────────────────────────┘ │
│ │
│ Category │
│ ┌──────────────────────────────────────┐ │
│ │ 🛒 Groceries › │ │ ← tap to change
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Mark as contribution to goal › │ │
│ └──────────────────────────────────────┘ │
│ │
│ Notes │
│ ┌──────────────────────────────────────┐ │
│ │ Tap to add note │ │
│ └──────────────────────────────────────┘ │
│ │
│ ⌄ Original SMS │ ← collapsible
│ ┌──────────────────────────────────────┐ │
│ │ RK31B7X4ZQ Confirmed. Ksh1,200.00 │ │ mono
│ │ paid to NAIVAS SUPERMARKET. on … │ │
│ └──────────────────────────────────────┘ │
│ │
│ [ Share ] [ Delete ] │
│ │
└──────────────────────────────────────────────┘
```
Notes:
- The "Mark as contribution" row appears only if user has at least one Goal with `Manual` rule.
- Delete only removes from app (the SMS remains in Android's SMS DB), and creates an UNDO snackbar.
### 4.10 In-app Update Flow
**Discovered (Home banner)** — slot just above Recent Activity:
```
┌──────────────────────────────────────────────┐
│ ✨ Ledga v1.1.0 is ready │
│ • Insights · Goals · Multi-SIM │
│ [ See what's new ] [ Update ›] │
└──────────────────────────────────────────────┘
```
- Background: `--accent-soft`, border: `--accent`.
- Dismissable. If dismissed it reappears in You → About.
**Full screen** — tap "See what's new":
```
┌──────────────────────────────────────────────┐
│ ‹ │
│ │
│ Ledga v1.1.0 │ Title L
│ March 2026 · 4.8 MB │ Body M muted
│ │
│ ┌──────────────────────────────────────────┐ │ hero illustration
│ │ [ release artwork ] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ What's new │
│ │
│ ✨ Insights tab — smart suggestions │
│ based on your spending patterns. │
│ │
│ 🎯 Goals — track savings towards rent, │
│ school fees, trips. │
│ │
│ Ⓢ Dual-SIM support — switch between │
│ two M-Pesa lines from the home screen. │
│ │
│ 🎨 Brand new design — bento layout, big │
│ numbers, dark theme polish. │
│ │
│ Fixes │
│ • Faster startup on Android 8 devices │
│ • Better Fuliza parsing │
│ • 14 small fixes from your feedback │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Download and install → │ │
│ └────────────────────────────────────┘ │
│ │
│ Remind me later │
└──────────────────────────────────────────────┘
```
**Installing** — replaces the CTA:
```
┌────────────────────────────────────┐
│ Downloading ▓▓▓▓▓▓▓▓░░░ 72% │
│ 3.4 MB of 4.8 MB │
└────────────────────────────────────┘
```
Then Android's standard installer intent fires.
---
## 5. Implementation Order
The redesign maps cleanly to phased PRs. Each phase is shippable.
### Phase A — Foundation (PR 1)
- Replace `Color.kt` with new token names + dark theme.
- Add `MpesaAccount` entity + DAO + migration.
- Add `Goal` entity + DAO + migration.
- Extend `Transaction` with `accountId: Long?`, `note: String?`.
- New `BentoCard`, `StatCard`, `Chip`, `ProgressRing` composables.
- New `TopBar` and `PillTabBar` composables.
### Phase B — Visual Overhaul (PR 2)
- Rewrite Home, Transactions, Trends with the new components, tokens, IA.
- New segmented "Activity" parent screen.
- New You screen consolidating Settings + new entries.
### Phase C — Insights (PR 3)
- Rule engine: anomaly (z-score against same-category rolling 4-week mean), recurring detection (3+ same merchant ≥21-day apart), fee tips, Fuliza warnings.
- `InsightsRepository` + `InsightWorker` (daily WorkManager).
- Insights screen + dismiss/snooze table.
### Phase D — Goals (PR 4)
- Goals screen + Add sheet + detail flow.
- Contribution attribution on transaction insert.
- "Mark as contribution" entry point in TransactionDetailSheet.
### Phase E — Multi-SIM (PR 5)
- Subscription detection on SMS receive (`SmsMessage.getSubscriptionId` / Android `SubscriptionManager`).
- Account switcher sheet.
- Filter on all queries via selected account or "combined".
- Onboarding step 4.
### Phase F — Update flow (PR 6)
- GitHub Releases polling.
- Banner + full-screen changelog.
- DownloadManager + install intent.
---
## 6. Open questions (decide before Phase A)
1. **Tab bar count**: keep 4 (Home / Activity / Insights / You) or 5 (split Trends out from Activity)? Recommendation: **4** — keeps thumb reach generous, Insights doesn't fight Trends.
2. **Multi-SIM detection**: Android only exposes `subscriptionId` on incoming SMS — not on already-imported ones. For historical imports we'll have to leave `accountId = null` and let the user assign in bulk. OK?
3. **Insight cadence**: notify on new insight (push) or only show in-app? Recommendation: **in-app only**, with a daily-summary mention if any new insights exist.
---
## 7. What's intentionally NOT in this redesign
- Web / desktop companion — out of scope, app stays mobile-only.
- Bill-pay shortcuts — Ledga is read-only; it tracks, not transacts.
- Bank-account integration — M-Pesa SMS only.
- Cloud sync between devices — backup-only via Google Drive; no per-device sync.
- AI summaries — Insights are deterministic rule output, not LLM output. Keeps the app offline and predictable.
-770
View File
@@ -1,770 +0,0 @@
# Ledga — M-Pesa Spending Tracker
## Overview
A native Android app that automatically tracks M-Pesa spending by reading SMS confirmations. Zero manual entry — install, grant SMS permission, done. Built for non-technical users with a clean, Canva-like UI.
**Target users:** Family members (all Android), non-technical
**Distribution:** Direct APK install + in-app self-updates via GitHub Releases (no Play Store)
**Data model:** Fully offline, local-only per device. No accounts, no server.
---
## Tech Stack
| Component | Technology |
|-----------|------------|
| Language | Kotlin |
| UI Framework | Jetpack Compose + Material 3 |
| Local Database | Room (SQLite) |
| Background SMS | BroadcastReceiver + WorkManager |
| DI | Hilt (dependency injection) |
| Settings Storage | Jetpack DataStore (Preferences) |
| Charts | Vico (Compose-native charting library) |
| Notifications | Android NotificationManager + AlarmManager |
| Backup | Google Drive API (auto), manual file export |
| Self-Update | GitHub Releases API |
| Min SDK | API 26 (Android 8.0) |
| Build | Gradle (Kotlin DSL) |
---
## Architecture
```
┌─────────────────────────────────────────────┐
│ UI Layer │
│ Jetpack Compose + Material 3 │
│ ┌──────┐ ┌──────────┐ ┌──────┐ ┌────────┐ │
│ │ Home │ │Transactions│ │Trends│ │Settings│ │
│ └──────┘ └──────────┘ └──────┘ └────────┘ │
├─────────────────────────────────────────────┤
│ ViewModel Layer │
│ StateFlow + Coroutines │
├─────────────────────────────────────────────┤
│ Repository Layer │
│ TransactionRepo │ CategoryRepo │ │
│ SettingsRepo │ BackupRepo │ │
├─────────────────────────────────────────────┤
│ Data Layer │
│ ┌──────┐ ┌───────────┐ ┌─────────────┐ │
│ │ Room │ │SMS Parser │ │Google Drive │ │
│ │ DB │ │ │ │ Backup │ │
│ └──────┘ └───────────┘ └─────────────┘ │
├─────────────────────────────────────────────┤
│ System Services │
│ SMS BroadcastReceiver │ WorkManager │
│ NotificationManager │ AlarmManager │
└─────────────────────────────────────────────┘
```
---
## SMS Parsing
### Sender Filter
Only process SMS from sender: `MPESA`
### Transaction Types & Patterns
**1. Send Money**
```
RK31B7X4ZQ Confirmed. Ksh500.00 sent to JOHN DOE 0712345678 on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh1,200.00. Transaction cost, Ksh0.00.
```
→ Extract: code, amount, recipient name, recipient phone, date/time, balance, cost
**2. Buy Goods (Till)**
```
RK31B7X4ZQ Confirmed. Ksh1,200.00 paid to NAIVAS SUPERMARKET. on 21/3/26 at 2:15 PM.New M-PESA balance is Ksh3,500.00. Transaction cost, Ksh0.00.
```
→ Extract: code, amount, merchant name, date/time, balance, cost
**3. Pay Bill**
```
RK31B7X4ZQ Confirmed. Ksh2,500.00 paid to KPLC PREPAID. Account Number 12345678. on 21/3/26 at 3:00 PM. New M-PESA balance is Ksh1,000.00. Transaction cost, Ksh0.00.
```
→ Extract: code, amount, business name, account number, date/time, balance, cost
**4. Withdraw (Agent)**
```
RK31B7X4ZQ Confirmed.You have withdrawn Ksh1,000.00 from JAMES AGENT 543210 on 21/3/26 at 4:00 PM.New M-PESA balance is Ksh500.00. Transaction cost, Ksh28.00.
```
→ Extract: code, amount, agent name, agent number, date/time, balance, cost
**5. Withdraw (ATM)**
```
RK31B7X4ZQ Confirmed. You have withdrawn Ksh5,000.00 from an ATM on 21/3/26 at 4:30 PM. New M-PESA balance is Ksh2,000.00. Transaction cost, Ksh34.00.
```
→ Extract: code, amount, date/time, balance, cost
→ Note: No agent name/number — just "from an ATM"
**6. Deposit**
```
RK31B7X4ZQ Confirmed.You have deposited Ksh5,000.00 to your M-PESA account on 21/3/26 at 10:00 AM.New M-PESA balance is Ksh5,500.00.
```
→ Extract: code, amount, date/time, balance
**7. Received Money**
```
RK31B7X4ZQ Confirmed.You have received Ksh2,000.00 from JANE DOE 0798765432 on 21/3/26 at 11:00 AM.New M-PESA balance is Ksh7,500.00.
```
→ Extract: code, amount, sender name, sender phone, date/time, balance
**8. Airtime Purchase (Self)**
```
RK31B7X4ZQ Confirmed. Ksh100.00 of airtime purchased on 21/3/26 at 12:00 PM.New M-PESA balance is Ksh7,400.00. Transaction cost, Ksh0.00.
```
→ Extract: code, amount, date/time, balance, cost
**9. Airtime Purchase (For Others)**
```
RK31B7X4ZQ Confirmed. You bought Ksh100.00 of airtime for 0712345678 on 21/3/26 at 12:30 PM.New M-PESA balance is Ksh7,300.00. Transaction cost, Ksh0.00.
```
→ Extract: code, amount, recipient phone, date/time, balance, cost
**10. M-Pesa Global (International Transfer)**
```
RK31B7X4ZQ Confirmed. Ksh5,000.00 sent to JOHN DOE +44712345678 (United Kingdom) via M-PESA Global on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh10,000.00. Transaction cost, Ksh150.00.
```
→ Extract: code, amount, recipient name, recipient phone (international), destination country, date/time, balance, cost
**11. Fuliza (Borrow)**
```
RK31B7X4ZQ Confirmed. Ksh500.00 sent to JOHN DOE 0712345678 on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh0.00. Fuliza M-PESA amount is Ksh500.00. Fuliza M-PESA outstanding amount is Ksh500.00.
```
→ Extract: all standard fields + fuliza amount, fuliza outstanding
**12. Fuliza Repayment**
```
RK31B7X4ZQ Confirmed. You have paid Ksh200.00 to Fuliza M-PESA on 21/3/26 at 2:00 PM. Fuliza M-PESA outstanding amount is Ksh300.00.
```
→ Extract: code, amount, date/time, fuliza outstanding
**13. Fuliza Reversal**
```
RK31B7X4ZQ Confirmed. Fuliza M-PESA of Ksh500.00 has been reversed on 21/3/26 at 3:00 PM. Fuliza M-PESA outstanding amount is Ksh0.00.
```
→ Extract: code, amount, date/time, fuliza outstanding
**14. Reversal**
```
RK31B7X4ZQ Confirmed. Transaction RJ12345678 has been reversed. Your account balance is Ksh2,000.00.
```
→ Extract: code, reversed transaction code, balance
### M-Shwari & KCB M-Pesa (Acknowledged, Not Parsed)
These services send SMS from the `MPESA` sender but are savings/loan products, not spending transactions:
- **M-Shwari:** `Confirmed. Ksh1,000.00 transferred to M-Shwari account...` / `Ksh1,000.00 transferred from M-Shwari...`
- **KCB M-Pesa:** `Confirmed. Ksh2,000.00 transferred to KCB M-Pesa account...` / loan disbursements
**Strategy:** Detect these by keywords ("M-Shwari", "KCB M-Pesa") and store with type `MSHWARI` or `KCB_MPESA`. Show them in the transaction list but don't count them as spending. They're internal transfers between M-Pesa wallet and savings/loan products.
### Parser Strategy
- Regex-based pattern matching per transaction type
- Keyword detection: "sent to", "paid to", "withdrawn.*from.*AGENT" (agent), "withdrawn.*from an ATM" (ATM), "deposited", "received", "airtime purchased", "bought.*airtime for", "M-PESA Global", "reversed", "Fuliza M-PESA amount", "paid to Fuliza", "Fuliza M-PESA of", "M-Shwari", "KCB M-Pesa"
- Amount extraction: `Ksh[\d,]+\.\d{2}` pattern
- Date extraction: `\d{1,2}/\d{1,2}/\d{2,4} at \d{1,2}:\d{2} [AP]M`
- International phone detection: `\+\d{7,15}` pattern (for M-Pesa Global)
- Country extraction: text in parentheses after international phone number
- Fallback: store unparsed M-Pesa SMS with type `UNKNOWN` for later parsing improvements
---
## Database Schema (Room)
### transactions
| Column | Type | Description |
|--------|------|-------------|
| id | Long (PK, auto) | Primary key |
| transactionCode | String (unique) | M-Pesa transaction code |
| type | Enum | SEND, BUY_GOODS, PAY_BILL, WITHDRAW_AGENT, WITHDRAW_ATM, DEPOSIT, RECEIVED, AIRTIME_SELF, AIRTIME_OTHER, MPESA_GLOBAL, FULIZA, FULIZA_REPAYMENT, FULIZA_REVERSAL, MSHWARI, KCB_MPESA, REVERSAL, UNKNOWN |
| amount | Double | Transaction amount in Ksh |
| transactionCost | Double | M-Pesa fee |
| recipientName | String? | Name of recipient/merchant |
| recipientPhone | String? | Phone number (if person) |
| accountNumber | String? | Account number (paybill) |
| destinationCountry | String? | Country name for M-Pesa Global transfers |
| balance | Double | M-Pesa balance after transaction |
| direction | Enum | INFLOW, OUTFLOW (derived at parse time — simplifies totals/chart queries) |
| categoryId | Long? (FK) | Assigned category |
| fulizaAmount | Double? | Fuliza amount (if applicable) |
| fulizaOutstanding | Double? | Fuliza outstanding (if applicable) |
| rawSms | String | Original SMS text (for re-parsing) |
| timestamp | Long | Transaction timestamp (epoch ms) |
| createdAt | Long | Record creation time |
### categories
| Column | Type | Description |
|--------|------|-------------|
| id | Long (PK, auto) | Primary key |
| name | String | Category name |
| icon | String | Material icon name |
| color | String | Hex color code |
| isDefault | Boolean | System default vs user-created |
### budgets
| Column | Type | Description |
|--------|------|-------------|
| id | Long (PK, auto) | Primary key |
| categoryId | Long? (FK) | Null = overall budget |
| monthlyLimit | Double | Budget limit in Ksh |
| isActive | Boolean | Whether alerts are enabled |
### category_rules
| Column | Type | Description |
|--------|------|-------------|
| id | Long (PK, auto) | Primary key |
| categoryId | Long (FK) | Target category |
| matchType | Enum | RECIPIENT_NAME, PHONE, TILL, PAYBILL |
| matchValue | String | Value to match |
### Default Categories
| Category | Icon | Color | Auto-match examples |
|----------|------|-------|---------------------|
| Groceries | shopping_cart | #4CAF50 | NAIVAS, QUICKMART, CARREFOUR |
| Transport | directions_car | #2196F3 | Uber, Bolt, matatu paybills |
| Bills & Utilities | receipt | #FF9800 | KPLC, NAIROBI WATER, DSTV |
| Airtime & Data | phone_android | #9C27B0 | Airtime purchases |
| Food & Dining | restaurant | #E91E63 | Restaurants, fast food |
| Send Money | person | #607D8B | Person-to-person transfers |
| Received | account_balance_wallet | #00BCD4 | Money received |
| Withdrawal | atm | #795548 | Agent withdrawals |
| Deposit | savings | #8BC34A | Cash deposits |
| Shopping | shopping_bag | #FF5722 | General merchants |
| International | public | #3F51B5 | M-Pesa Global transfers |
| Savings & Loans | account_balance | #009688 | M-Shwari, KCB M-Pesa |
| Other | category | #9E9E9E | Uncategorized |
---
## Screens & UI
### Design System
- **Color palette:** M-Pesa green (#4CAF50) as primary, warm neutrals, category-specific accent colors
- **Typography:** System font, scalable (Small/Medium/Large/Extra Large + System Default)
- **Cards:** Rounded corners (16dp), subtle elevation, clean spacing
- **Animations:** Smooth transitions between screens, animated chart loading
- **Dark mode:** Full dark theme, follows system by default with manual override
### 1. Home Dashboard
```
┌─────────────────────────────────┐
│ Good morning 👋 │
│ │
│ M-PESA Balance │
│ Ksh 12,500.00 │
│ (from last transaction) │
│ │
│ ┌─────────────────────────┐ │
│ │ March 2026 │ │
│ │ Spent: Ksh 45,200 │ │
│ │ ▼ 12% vs last month │ │
│ │ Fees: Ksh 320 │ │
│ └─────────────────────────┘ │
│ │
│ [Today] [This Week] [Month] │
│ │
│ ┌─────────────────────────┐ │
│ │ 🍩 Donut Chart │ │
│ │ Category Breakdown │ │
│ └─────────────────────────┘ │
│ │
│ Recent Transactions │
│ ┌─────────────────────────┐ │
│ │ 🛒 Naivas -Ksh 1,200 │ │
│ │ 📱 Airtime -Ksh 100 │ │
│ │ 💰 Received +Ksh 5,000 │ │
│ └─────────────────────────┘ │
│ │
│ ┌────┐┌────┐┌────┐┌────┐ │
│ │Home││List││Stats││ ⚙️ │ │
│ └────┘└────┘└────┘└────┘ │
└─────────────────────────────────┘
```
> **Balance display:** Shows the `balance` field from the most recent transaction. This is the last known M-Pesa balance — not a live query. Labeled "from last transaction" so users don't confuse it with a real-time check.
### 2. Transactions List
- Grouped by day with day totals
- Each item: category icon + recipient/merchant + amount (red spent, green received)
- Search bar at top
- Filter chips: All, Sent, Received, Bills, Goods, Withdraw
- Tap → transaction detail bottom sheet
- Long press → change category
### 3. Trends / Analytics
- Bar chart: daily spending for current month
- Line chart: monthly spending over time
- Top 5 recipients/merchants list
- Category comparison vs previous period
- **Transaction fees summary** — total fees paid this period (most people don't realize how much they spend on M-Pesa fees)
- Period selector: 7D / 30D / 90D / 1Y
### 4. Settings
```
Appearance
├── Theme: System / Light / Dark
└── Font Size: System Default / Small / Medium / Large / Extra Large
Notifications
├── Daily Summary: ON/OFF + time picker
├── Weekly Summary: ON/OFF
├── Budget Alerts: ON/OFF
└── Large Transaction Alert: ON/OFF + threshold (Ksh)
Budgets
├── Overall Monthly Budget: Ksh ___
└── Per Category: [list of categories with limits]
Data
├── Import SMS History
├── Export Data (CSV/JSON)
├── Unparsed Messages (X) — view SMS that failed to parse
└── Clear All Data (with confirmation)
Backup
├── Auto Backup: ON/OFF
├── Backup Frequency: Daily / Weekly
├── Google Account: shown
├── Last Backup: date
├── Back Up Now
└── Restore from Backup
About
├── Version: 1.0.0
├── Check for Updates
└── What's New
```
---
## Notification System
| Notification | Trigger | Default |
|-------------|---------|---------|
| Daily Summary | Scheduled (user picks time, default 8 PM) | ON |
| Weekly Summary | Every Sunday evening | ON |
| Budget Warning | 80% of budget reached | ON |
| Budget Exceeded | 100% of budget reached | ON |
| Large Transaction | Single transaction > threshold | OFF (default Ksh 5,000) |
Implementation:
- WorkManager for periodic summary jobs
- Immediate check on each new transaction for budget/large amount alerts
- NotificationChannel: "Spending Summaries", "Budget Alerts"
---
## Backup System
### Google Drive Auto-Backup
- Uses Google Sign-In (already on every Android phone)
- Backs up Room database + settings to app-specific Google Drive folder
- No extra permissions needed (app data folder is automatic)
- Frequency: daily or weekly (user choice)
- On fresh install: detect existing backup → prompt restore
### Manual Export/Import
- Export: generates a ZIP file containing:
- `transactions.csv` — human-readable, can open in Excel
- `data.json` — full database export for app restore
- Import: select ZIP file → restore all data
- Share via WhatsApp, Bluetooth, email, etc.
### SMS Re-import (Fallback)
- Scan all existing SMS from "MPESA" sender
- Parse and import, skipping duplicates (by transaction code)
- Progress indicator during import
---
## Self-Update System
### Flow
1. App checks GitHub Releases API on launch (max once per day)
2. Compares current version with latest release tag
3. If newer version exists:
- Shows non-intrusive banner: "Update available (v1.2.0)"
- Tap → shows changelog (from release notes)
- "Download & Install" button
4. Downloads APK to app cache
5. Triggers Android package installer intent
6. User confirms install (standard Android prompt)
### GitHub Release Format
```
Tag: v1.0.0
Title: Version 1.0.0
Body: Changelog in markdown
Asset: ledga-v1.0.0.apk
```
### Endpoint
```
GET https://api.github.com/repos/{owner}/{repo}/releases/latest
```
---
## Auto-Categorization
### Rule Engine (Priority Order)
1. **User rules** — if user manually categorized a merchant before, remember it
2. **Category rules table** — pattern matching on recipient name, till number, paybill
3. **Transaction type fallback** — airtime → Airtime, withdrawal → Withdrawal, etc.
4. **Default** — "Other" category
### Pre-loaded Rules
| Pattern | Match Type | Category |
|---------|------------|----------|
| NAIVAS, QUICKMART, CARREFOUR, CLEANSHELF | Recipient | Groceries |
| KPLC, KENYA POWER | Recipient | Bills |
| 888880 (KPLC Prepaid), 888888 (KPLC Postpaid) | Paybill | Bills |
| NAIROBI WATER, ELDORET WATER | Recipient | Bills |
| 444400 (Nairobi Water) | Paybill | Bills |
| DSTV, GOTV, SHOWMAX | Recipient | Bills |
| UBER, BOLT, LITTLE | Recipient | Transport |
| JAVA, KFC, CHICKEN INN, PIZZA INN | Recipient | Food & Dining |
| (any self airtime) | Type | Airtime |
| (any airtime for others) | Type | Airtime |
| (any withdrawal) | Type | Withdrawal |
| (any deposit) | Type | Deposit |
| (any received) | Type | Received |
| (any send money) | Type | Send Money |
| (any M-Pesa Global) | Type | International |
| (any M-Shwari) | Type | Savings & Loans |
| (any KCB M-Pesa) | Type | Savings & Loans |
Users can add/edit rules from the category settings.
---
## Permissions Required
| Permission | Purpose | When Requested |
|-----------|---------|----------------|
| RECEIVE_SMS | Listen for new M-Pesa SMS in real-time | On first launch |
| READ_SMS | Import existing M-Pesa SMS history | When user taps "Import History" |
| POST_NOTIFICATIONS | Daily summaries, budget alerts | On first launch (Android 13+) |
| INTERNET | Google Drive backup, update checks | Automatic |
| REQUEST_INSTALL_PACKAGES | Self-update APK installation | When user taps "Install Update" |
---
## Project Structure
```
ledga/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/ledga/app/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── LedgaApp.kt
│ │ │ │ ├── di/
│ │ │ │ │ ├── AppModule.kt
│ │ │ │ │ └── DatabaseModule.kt
│ │ │ │ ├── data/
│ │ │ │ │ ├── db/
│ │ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ │ ├── Migrations.kt
│ │ │ │ │ │ ├── dao/
│ │ │ │ │ │ │ ├── TransactionDao.kt
│ │ │ │ │ │ │ ├── CategoryDao.kt
│ │ │ │ │ │ │ └── BudgetDao.kt
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── Transaction.kt
│ │ │ │ │ │ ├── Category.kt
│ │ │ │ │ │ ├── Budget.kt
│ │ │ │ │ │ └── CategoryRule.kt
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ ├── TransactionRepository.kt
│ │ │ │ │ │ ├── CategoryRepository.kt
│ │ │ │ │ │ └── SettingsRepository.kt
│ │ │ │ │ └── parser/
│ │ │ │ │ └── MpesaSmsParser.kt
│ │ │ │ ├── receiver/
│ │ │ │ │ └── SmsReceiver.kt
│ │ │ │ ├── worker/
│ │ │ │ │ ├── NotificationWorker.kt
│ │ │ │ │ ├── BackupWorker.kt
│ │ │ │ │ └── UpdateCheckWorker.kt
│ │ │ │ ├── ui/
│ │ │ │ │ ├── theme/
│ │ │ │ │ │ ├── Theme.kt
│ │ │ │ │ │ ├── Color.kt
│ │ │ │ │ │ └── Type.kt
│ │ │ │ │ ├── navigation/
│ │ │ │ │ │ └── AppNavigation.kt
│ │ │ │ │ ├── onboarding/
│ │ │ │ │ │ ├── OnboardingScreen.kt
│ │ │ │ │ │ └── OnboardingViewModel.kt
│ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ │ └── HomeViewModel.kt
│ │ │ │ │ ├── transactions/
│ │ │ │ │ │ ├── TransactionsScreen.kt
│ │ │ │ │ │ ├── TransactionDetailSheet.kt
│ │ │ │ │ │ └── TransactionsViewModel.kt
│ │ │ │ │ ├── trends/
│ │ │ │ │ │ ├── TrendsScreen.kt
│ │ │ │ │ │ └── TrendsViewModel.kt
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── SettingsScreen.kt
│ │ │ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ │ │ └── UnparsedSmsScreen.kt
│ │ │ │ │ └── components/
│ │ │ │ │ ├── TransactionCard.kt
│ │ │ │ │ ├── CategoryChip.kt
│ │ │ │ │ ├── SpendingChart.kt
│ │ │ │ │ └── BudgetProgressBar.kt
│ │ │ │ └── util/
│ │ │ │ ├── CurrencyFormatter.kt
│ │ │ │ └── DateUtils.kt
│ │ │ ├── res/
│ │ │ └── AndroidManifest.xml
│ │ └── test/
│ │ └── java/com/ledga/app/
│ │ └── parser/
│ │ └── MpesaSmsParserTest.kt
│ └── build.gradle.kts
├── build.gradle.kts
├── settings.gradle.kts
└── README.md
```
---
## Build Phases
### Phase 1 — Core (MVP)
- [ ] Project setup (Kotlin, Compose, Room, Hilt, Gradle)
- [ ] SMS parser — handle all M-Pesa transaction types (including M-Pesa Global, M-Shwari, KCB M-Pesa)
- [ ] SMS parser unit tests — real SMS samples for every transaction type
- [ ] Room database — transactions, categories, category rules, migrations setup
- [ ] SMS BroadcastReceiver — real-time capture
- [ ] SMS history import
- [ ] Onboarding flow — welcome screen, SMS permission with plain-language explanation, optional history import
- [ ] Home dashboard — current balance, spending total, fees total, donut chart, recent transactions
- [ ] Transactions list — grouped by day, search, filter
- [ ] Basic auto-categorization with default rules (name + paybill/till matching)
- [ ] Bottom navigation (Home, Transactions, Trends, Settings)
- [ ] Theme system (System/Light/Dark)
### Phase 2 — Polish
- [ ] Trends screen — bar charts, line charts, top merchants, fees summary, period comparison
- [ ] Font size settings (System/Small/Medium/Large/Extra Large)
- [ ] Budget system — set limits, track progress bars
- [ ] Category management — add/edit/delete custom categories
- [ ] Transaction detail bottom sheet
- [ ] Long press to re-categorize
- [ ] Category rules editor
- [ ] Unparsed SMS viewer (Settings → Data)
### Phase 3 — Notifications & Backup
- [ ] Daily spending summary notification
- [ ] Weekly summary notification
- [ ] Budget alert notifications
- [ ] Large transaction alerts
- [ ] Notification settings (toggles, time pickers)
- [ ] Google Drive auto-backup
- [ ] Google Drive restore on fresh install
- [ ] Manual export (CSV + JSON zip)
- [ ] Manual import
### Phase 4 — Distribution
- [ ] APK signing setup — generate keystore, document secure storage
- [ ] GitHub Actions CI/CD — build release APK on push to main
- [ ] GitHub Releases self-update system
- [ ] Update check on launch
- [ ] Changelog display
- [ ] APK download + install flow
- [ ] App icon and splash screen
- [ ] Final UI polish and animations
---
## App Name
**Ledga** — a play on "ledger". Short, unique, easy to say.
- Package name: `com.ledga.app`
---
## Onboarding Flow
First launch experience for non-technical users. Only shown once — a `hasCompletedOnboarding` flag in DataStore skips it on subsequent launches.
```
┌─────────────────────────────────┐
│ │
│ Welcome to Ledga │
│ │
│ Track your M-Pesa spending │
│ automatically. No typing, │
│ no manual entry. │
│ │
│ [Get Started →] │
│ │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ │
│ Ledga reads your M-Pesa SMS │
│ to track your spending. │
│ │
│ Your messages never leave │
│ your phone. Everything stays │
│ private and offline. │
│ │
│ [Allow SMS Access →] │
│ │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ │
│ Import your M-Pesa history? │
│ │
│ We can scan your existing │
│ M-Pesa messages to build │
│ your spending history. │
│ │
│ [Import History] │
│ [Skip — start fresh] │
│ │
│ ┌─────────────────────┐ │
│ │ ████████░░ 67% │ │
│ │ Found 342 transactions│ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ │
│ You're all set! │
│ │
│ Ledga will now track your │
│ M-Pesa spending in the │
│ background. │
│ │
│ [Go to Dashboard →] │
│ │
└─────────────────────────────────┘
```
---
## Testing Strategy
### Unit Tests (Critical)
- **SMS Parser** — test every transaction type with real M-Pesa SMS samples
- At least 3 variations per type (different amounts, names, edge cases)
- Malformed messages (truncated SMS, missing fields)
- Messages from non-M-Pesa senders (should be ignored)
- Fuliza messages embedded in send/buy goods SMS
- M-Pesa Global with various country formats
- M-Shwari/KCB M-Pesa detection
- Amount parsing edge cases: `Ksh1.00`, `Ksh100,000.00`
- **Auto-categorization** — rule matching priority, paybill matching, fallback behavior
- **CurrencyFormatter** — Ksh formatting, large numbers, zero amounts
### Integration Tests
- Room database — DAO queries, migrations, duplicate transaction code handling
- SMS import — bulk parsing performance, duplicate skipping
### Manual Testing
- Collect real M-Pesa SMS from family members (anonymized) as test fixtures
- Test on low-end Android devices (target audience likely has budget phones)
---
## APK Signing & CI/CD
### Keystore
- Path: `mobile/ledga/keystore/ledga-release.jks` (gitignored)
- Key alias: `ledga`
- Generate a release keystore and store it securely
- **CRITICAL:** Back up the keystore — if lost, existing installs can never receive updates (Android requires consistent signing). The VPS backup job does not cover this file; back it up manually to a password manager or offline storage.
- Store keystore password and key alias in a secure location (not in the repo)
### Build Commands
```bash
cd ~/dev/mobile/ledga
# Debug APK
./gradlew assembleDebug
# -> app/build/outputs/apk/debug/app-debug.apk
# Release APK (signed, requires keystore + secrets)
./gradlew assembleRelease
# -> app/build/outputs/apk/release/app-release.apk
# Tests
./gradlew testDebugUnitTest
```
### Release a Version
```bash
git tag v1.0.0 && git push origin v1.0.0
# GitHub Action builds signed APK and creates release automatically
```
### GitHub Actions
```yaml
# .github/workflows/release.yml
# Triggered on push to main or manual dispatch
# Steps:
# 1. Checkout code
# 2. Set up JDK 17
# 3. Decode keystore from GitHub secret
# 4. Build release APK (assembleRelease)
# 5. Sign APK
# 6. Create GitHub Release with APK asset
```
### GitHub Secrets Required
| Secret | Purpose |
|--------|---------|
| `KEYSTORE_BASE64` | Base64-encoded release keystore |
| `KEYSTORE_PASSWORD` | Keystore password |
| `KEY_ALIAS` | Key alias name |
| `KEY_PASSWORD` | Key password |
---
## Database Migrations
Schema changes are inevitable as the app evolves. Room requires explicit migrations to preserve user data.
### Strategy
- **Never use `fallbackToDestructiveMigration()`** — this destroys all user data on schema change
- Define all migrations in `Migrations.kt` as `Migration(oldVersion, newVersion)` objects
- Test every migration path with Room's `MigrationTestHelper`
- Keep a version history:
| Version | Changes |
|---------|---------|
| 1 | Initial schema (transactions, categories, budgets, category_rules) |
### Example Migration Pattern
```kotlin
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE transactions ADD COLUMN newField TEXT")
}
}
```
---
## Notes
- All data stays on device — no external server, no analytics, no tracking
- SMS parsing must be thoroughly tested with real M-Pesa SMS variations
- Fuliza messages have extra fields — parser must handle gracefully
- Fuliza has 3 distinct message types: borrow (Fuliza M-PESA amount), repayment (paid to Fuliza), and reversal — each has a different format
- Some M-Pesa SMS formats may vary slightly — keep raw SMS stored for re-parsing
- Transaction code uniqueness prevents duplicate imports
- Multi-SIM: some users have two M-Pesa lines — consider adding a `simSlot` or `phoneNumber` field on transactions (extracted from SMS subscription ID) if multi-line tracking is needed later
- Google Drive backup uses `appDataFolder` scope — backups are invisible to the user in Drive. This is by design (no extra permissions), but users should know they won't find the file browsing Drive manually
- The `direction` column (INFLOW/OUTFLOW) is set at parse time based on transaction type, avoiding repeated derivation in every query for totals and charts