A cross-platform expense tracking and debt management app for friends and groups
Consolidate and track shared expenses with friends and groups. Split bills, settle debts, and keep everyone on the same page.
| Name | |
|---|---|
| Dylan Dai | dylan.dai@uwaterloo.ca |
| Hanz Po | hnqpo@uwaterloo.ca |
| Daniel Su | d2su@uwaterloo.ca |
| Cristophe Chen | c253chen@uwaterloo.ca |
Click the badge above to watch a full 20 minute walkthrough of all of Ledger's features.
Click the badge above to watch a short 2 minute walkthrough of Ledger's main features.
| Dashboard | Friends & Groups | Debts & Expenses |
|---|---|---|
| New Expense | Friend Profile | My Profile |
|---|---|---|
| Library | Purpose | Link |
|---|---|---|
| Compose Multiplatform | Cross-platform UI framework | JetBrains |
| Supabase | Backend-as-a-Service (PostgreSQL, Auth, Storage) | Supabase |
| Ktor Client | HTTP networking for API calls | Ktor |
| Kotlin Serialization | JSON serialization/deserialization | Kotlinx |
| Navigation Compose | Screen navigation | Android Developers |
| Coil | Image loading (Android) | Coil |
| Material 3 | Modern UI components and theming | Material Design |
- Phone number formatting adapted from libphonenumber patterns
- Currency formatting utilities based on standard ISO-4217 patterns
- Video player implementation references Apple AVFoundation documentation
| Version | Date | Download | Release Notes |
|---|---|---|---|
| v1.0.0 | Dec 2024 | Android APK, iOS IPA | Release Notes |
See the Release Notes for full details on features, known issues, and future improvements.
- Team Contract - Our working agreement and team policies
- Project Proposal - Original project proposal and feature planning
- Meeting Minutes - Log of all team meetings
- Developer Logs - Individual development journals
- Team Reflections - End-of-term retrospective and lessons learned
- β Android (API 24+, Android 7.0 Nougat and above)
- β iOS (iOS 14.1+)
π± Android Installation
Option 1: Install APK on Physical Device
- Download
ledger-v1.0.1.apkfrom the Releases section - Transfer the APK to your Android device
- Open the APK file and follow the installation prompts
- You may need to enable "Install from unknown sources" in Settings
Option 2: Android Virtual Device (AVD)
- Open Android Studio
- Go to Tools β Device Manager β Create Virtual Device
- Select a device (e.g., Pixel 6) and download a system image (API 24+)
- Start the emulator
- Drag and drop the APK file onto the emulator window
- The app will install and appear in the app drawer
π iOS Installation (macOS only)
- Ensure you have Xcode 14.1+ installed
- Clone the repository:
git clone https://git.uwaterloo.ca/d27dai/ledger.git cd ledger - Build the iOS framework:
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
- Open the iOS project:
open iosApp/iosApp.xcodeproj
- Select your target device (Simulator or physical device)
- Click Run
βΆοΈ in Xcode
π§ Building from Source
Prerequisites:
- JDK 11 or higher
- Android Studio (latest) or IntelliJ IDEA
- Kotlin 2.0.20+
- Gradle 8.5+ (included via wrapper)
Steps:
- Clone the repository
- Open in Android Studio/IntelliJ
- Wait for Gradle sync to complete
- Select
composeAppconfiguration - Choose target device and run
Command Line Build:
# Android Debug APK
./gradlew :composeApp:assembleDebug
# Install on connected device
./gradlew :composeApp:installDebug
# Run all tests
./gradlew test- Launch the app - You'll see the landing screen with a video background
- Create Account - Tap "Create Account" to sign up with your phone number
- Verify OTP - Enter the 6-digit code sent to your phone
- Complete Profile - Enter your name and choose a unique username
- Permissions - Optionally enable notifications and camera access
| Action | How To |
|---|---|
| Add Friend | Tap the + button on Friends screen β Search by username or phone |
| View Friend Profile | Tap on a friend's name in the list |
| Send Friend Request | Find user via search β Tap "Add Friend" button |
| Accept Requests | Go to Friend Requests β Tap β to accept |
- Navigate to the Friends tab
- Tap on a friend to open the Ledger (expense conversation)
- Tap + to add a new expense
- Enter amount, description, and optionally attach a receipt
- The debt appears in both users' ledgers
Groups allow multiple people to track shared expenses (e.g., roommates, trips):
- Go to the Groups tab (from Friends screen header)
- Tap Create Group β Enter name and description
- Invite Members β Search friends and add them
- Add Group Expenses β Open group ledger and add shared costs
- View Balances β See who owes what in the group
- Upload Receipts: Use the camera or gallery to attach receipt images
- View History: Check the Transactions screen for payment history
- Settle Debts: Mark debts as paid when settled
The database schema is hosted on Supabase (PostgreSQL). Below is the ERD showing all entities and relationships:
erDiagram
users {
TEXT id PK
TEXT name
TEXT phone_number
TEXT username
TEXT avatar_url
BOOLEAN onboarding_completed
BIGINT created_at
}
friends {
TEXT id PK
TEXT user_id FK
TEXT name
TEXT username
TEXT phone_number
TEXT avatar_url
TEXT linked_user_id FK
BIGINT created_at
}
friend_requests {
TEXT id PK
TEXT from_user_id FK
TEXT to_user_id FK
TEXT status
BIGINT created_at
}
groups {
TEXT id PK
TEXT name
TEXT description
TEXT image_url
TEXT leader_id FK
BOOLEAN is_active
BIGINT created_at
BIGINT updated_at
}
group_members {
TEXT id PK
TEXT group_id FK
TEXT user_id FK
TEXT role
BOOLEAN is_active
BIGINT joined_at
BIGINT left_at
}
debts {
TEXT id PK
TEXT user_id FK
TEXT friend_id FK
TEXT group_id FK
DOUBLE amount
TEXT description
TEXT category
TEXT currency
TEXT location
BOOLEAN is_settled
BIGINT date
TEXT receipt_id FK
TEXT debt_group_id FK
BIGINT created_at
BIGINT updated_at
}
debt_groups {
TEXT id PK
TEXT user_id FK
TEXT friend_id FK
TEXT group_id FK
TEXT label
TEXT description
BIGINT created_at
BIGINT updated_at
}
receipts {
TEXT id PK
TEXT user_id FK
TEXT image_url
TEXT vendor_name
DOUBLE total_amount
TEXT currency
TEXT notes
BIGINT date
BIGINT created_at
}
transactions {
TEXT id PK
TEXT debt_id FK
TEXT payer_id FK
TEXT payee_id FK
DOUBLE amount
TEXT description
TEXT type
TEXT notes
BIGINT timestamp
}
users ||--o{ friends : "has"
users ||--o{ friend_requests : "sends"
users ||--o{ friend_requests : "receives"
users ||--o{ groups : "leads"
users ||--o{ group_members : "joins"
users ||--o{ debts : "owns"
users ||--o{ receipts : "uploads"
users ||--o{ transactions : "pays"
users ||--o{ transactions : "receives"
friends }o--|| users : "linked_to"
friends ||--o{ debts : "involved_in"
friends ||--o{ debt_groups : "has"
groups ||--o{ group_members : "contains"
groups ||--o{ debts : "tracks"
groups ||--o{ debt_groups : "organizes"
debts }o--o| receipts : "attached_to"
debts }o--o| debt_groups : "grouped_in"
debts ||--o{ transactions : "generates"
The application follows a Layered Architecture with clear separation of concerns:
classDiagram
direction TB
%% ===== PRESENTATION LAYER =====
namespace PresentationLayer {
class HomeScreen {
+navController: NavHostController
+authRepo: SupabaseAuthDataSource
+debtUseCases: DebtUseCases
-totalYouOwe: Double
-totalOwedToYou: Double
+HomeScreen()
}
class FriendsScreen {
+navController: NavHostController
-friends: List~Friend~
-searchQuery: String
+FriendsScreen()
}
class DebtsScreen {
+navController: NavHostController
-debts: List~Debt~
+DebtsScreen()
}
class GroupsScreen {
+navController: NavHostController
-groups: List~Group~
+GroupsScreen()
}
class LedgerScreen {
+navController: NavHostController
+friendId: String
-debts: List~Debt~
+LedgerScreen()
}
class ProfileScreen {
+navController: NavHostController
-user: User
+ProfileScreen()
}
}
%% ===== DOMAIN LAYER - USE CASES =====
namespace DomainLayer_UseCases {
class DebtUseCases {
-debtDataSource: SupabaseDebtDataSource
-transactionUseCases: TransactionUseCases
-friendDataSource: FriendRepository
+createDebtWithFriend() Result~Debt~
+createGroupDebt() Result~Debt~
+getAllDebts() Result~List~
+getDebtsWithFriend() Result~List~
+getGroupDebts() Result~List~
+settleDebt() Result~Debt~
+updateDebtAmount() Result~Debt~
+deleteDebt() Result~Unit~
+calculateTotalOwed() Result~Double~
+calculateTotalOwedToUser() Result~Double~
}
class FriendUseCases {
-friendDataSource: SupabaseFriendDataSource
+createFriend() Result~Friend~
+getFriends() Result~List~
+searchFriends() Result~List~
+deleteFriend() Result~Unit~
}
class GroupUseCases {
-groupDataSource: SupabaseGroupDataSource
-memberDataSource: SupabaseGroupMemberDataSource
+createGroup() Result~Group~
+getGroups() Result~List~
+addMember() Result~GroupMember~
+removeMember() Result~Unit~
+leaveGroup() Result~Unit~
}
class TransactionUseCases {
-transactionDataSource: SupabaseTransactionDataSource
+createTransaction() Result~Transaction~
+getTransactions() Result~List~
}
class ReceiptUseCases {
-receiptDataSource: SupabaseReceiptDataSource
+uploadReceipt() Result~Receipt~
+getReceipts() Result~List~
}
}
%% ===== DOMAIN LAYER - MODELS =====
namespace DomainLayer_Models {
class User {
+id: String
+name: String
+phoneNumber: String
+username: String?
+avatarUrl: String?
+onboardingCompleted: Boolean
+createdAt: Long
+isProfileComplete() Boolean
+getDisplayName() String
}
class Friend {
+id: String
+userId: String
+name: String
+username: String?
+phoneNumber: String?
+linkedUserId: String?
+avatarUrl: String?
+createdAt: Long
+hasContactInfo() Boolean
+isRegisteredUser() Boolean
}
class Debt {
+id: String
+userId: String
+friendId: String?
+groupId: String?
+amount: Double
+description: String
+category: DebtCategory
+currency: String
+isSettled: Boolean
+receiptId: String?
+userOwes() Boolean
+friendOwes() Boolean
+settle() Debt
+hasReceipt() Boolean
}
class Group {
+id: String
+name: String
+description: String?
+imageUrl: String?
+leaderId: String
+isActive: Boolean
+isValid() Boolean
+deactivate() Group
+rename() Group
}
class GroupMember {
+id: String
+groupId: String
+userId: String
+role: GroupRole
+isActive: Boolean
+isOwner() Boolean
+canInvite() Boolean
+canKickMembers() Boolean
+leave() GroupMember
}
class Receipt {
+id: String
+userId: String
+imageUrl: String?
+vendorName: String?
+totalAmount: Double
+currency: String
+hasImage() Boolean
+getSummary() String
}
class Transaction {
+id: String
+debtId: String
+payerId: String
+payeeId: String
+amount: Double
+type: TransactionType
+isPayment() Boolean
+getSummary() String
}
}
%% ===== DATA LAYER =====
namespace DataLayer {
class SupabaseAuthDataSource {
+signInWithPhone() Result~Unit~
+verifyOtp() Result~AuthUser~
+getCurrentUser() AuthUser?
+getAuthState() Flow~AuthState~
+signOut() Result~Unit~
}
class SupabaseDebtDataSource {
+getAllDebts() List~Debt~
+getDebtsWithFriend() List~Debt~
+getDebtsInGroup() List~Debt~
+saveDebt() Unit
+updateDebt() Unit
+deleteDebt() Unit
}
class SupabaseFriendDataSource {
+getAllFriends() List~Friend~
+getFriend() Friend?
+saveFriend() Unit
+deleteFriend() Unit
}
class SupabaseGroupDataSource {
+getAllGroups() List~Group~
+getGroup() Group?
+saveGroup() Unit
+updateGroup() Unit
}
class SupabaseUserDataSource {
+getUser() User?
+saveUser() Unit
+updateUser() Unit
+searchByUsername() List~User~
}
}
%% ===== REPOSITORY INTERFACES =====
namespace RepositoryInterfaces {
class DebtRepository {
<<interface>>
+getAllDebts() List~Debt~
+getDebt() Debt?
+saveDebt() Unit
+updateDebt() Unit
+deleteDebt() Unit
}
class FriendRepository {
<<interface>>
+getAllFriends() List~Friend~
+getFriend() Friend?
+saveFriend() Unit
+deleteFriend() Unit
}
class GroupRepository {
<<interface>>
+getAllGroups() List~Group~
+getGroup() Group?
+saveGroup() Unit
}
}
%% ===== RELATIONSHIPS =====
%% Screens use Use Cases
HomeScreen --> DebtUseCases
HomeScreen --> SupabaseAuthDataSource
FriendsScreen --> FriendUseCases
DebtsScreen --> DebtUseCases
GroupsScreen --> GroupUseCases
LedgerScreen --> DebtUseCases
ProfileScreen --> SupabaseUserDataSource
%% Use Cases use Data Sources
DebtUseCases --> SupabaseDebtDataSource
DebtUseCases --> TransactionUseCases
FriendUseCases --> SupabaseFriendDataSource
GroupUseCases --> SupabaseGroupDataSource
TransactionUseCases --> SupabaseTransactionDataSource
ReceiptUseCases --> SupabaseReceiptDataSource
%% Data Sources implement Repositories
SupabaseDebtDataSource ..|> DebtRepository
SupabaseFriendDataSource ..|> FriendRepository
SupabaseGroupDataSource ..|> GroupRepository
%% Model relationships
User "1" --> "*" Friend : owns
User "1" --> "*" Debt : creates
User "1" --> "*" Group : leads
Friend "1" --> "*" Debt : involved
Group "1" --> "*" GroupMember : has
Group "1" --> "*" Debt : tracks
Debt "*" --> "0..1" Receipt : attaches
Debt "1" --> "*" Transaction : generates
The application is fully functional and can be run using the instructions in the Getting Started section.
The app requires a local.properties file in the project root. Since this file is gitignored, copy from the provided example:
cp local.properties.example local.properties
# Then edit local.properties to set your Android SDK pathThe example file includes all required credentials:
SUPABASE_URLandSUPABASE_ANON_KEYfor backendOPENAI_API_KEYfor receipt OCR
The app uses a hosted Supabase instance - no local database setup is required. The Supabase credentials are configured in local.properties.
Supabase Project URL: https://kkvyojihkdshlobtyvqp.supabase.co
For grading purposes, you can create a new account using any valid phone number, or use the existing test flow:
- Launch the app
- Tap "Create Account"
- Enter a phone number (SMS verification will be sent)
- Complete the onboarding flow
| Feature | Navigation | Description |
|---|---|---|
| User Registration | Landing β Create Account | Phone-based auth with OTP |
| Friend Management | Friends tab β + button | Add, search, accept friend requests |
| Debt Creation | Friends β Friend β + button | Create expenses with a friend |
| Group Expenses | Friends β Groups β Create | Multi-user expense sharing |
| Receipt Upload | + button (center) | Camera/gallery image attachment |
| Transaction History | Home β Transactions | View payment history |
| Profile Management | Profile tab | Edit name, username, avatar |
# Build Android APK
./gradlew :composeApp:assembleDebug
# Run unit tests
./gradlew :shared:testDebugUnitTest
# Install on connected device
./gradlew :composeApp:installDebugledger/
βββ composeApp/ # UI layer (Compose Multiplatform screens)
β βββ src/commonMain/ # Shared UI code for Android & iOS
βββ shared/ # Business logic layer
β βββ src/commonMain/
β βββ domain/ # Models and Use Cases
β βββ data/ # Repositories and Data Sources
βββ iosApp/ # iOS app wrapper (Xcode project)
βββ database/ # SQL schema and migrations
- The app requires network access to communicate with Supabase
- Phone verification uses real SMS - ensure you have a valid phone number
- All data is stored remotely in Supabase PostgreSQL
- Row Level Security (RLS) ensures users can only access their own data
This project was created as part of CS 346 at the University of Waterloo.
Happy Tracking! π°