Skip to content

tophuu/ledger

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

131 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Ledger

A cross-platform expense tracking and debt management app for friends and groups

Ledger Landing Screen

Consolidate and track shared expenses with friends and groups. Split bills, settle debts, and keep everyone on the same page.


Team 102-04

Name Email
Dylan Dai dylan.dai@uwaterloo.ca
Hanz Po hnqpo@uwaterloo.ca
Daniel Su d2su@uwaterloo.ca
Cristophe Chen c253chen@uwaterloo.ca

🎬 Demo Video

Ledger Demo Video

Click the badge above to watch a full 20 minute walkthrough of all of Ledger's features.

1-2 minute version

Click the badge above to watch a short 2 minute walkthrough of Ledger's main features.


πŸ“Έ Screenshots

Dashboard Friends & Groups Debts & Expenses
New Expense Friend Profile My Profile

Acknowledgements

External Libraries

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

External Code References

  • Phone number formatting adapted from libphonenumber patterns
  • Currency formatting utilities based on standard ISO-4217 patterns
  • Video player implementation references Apple AVFoundation documentation

Releases

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.


πŸ“š Project Information

Planning Documents

Progress Tracking

Reflections


πŸ“– User Guide

Getting Started

Supported Platforms

  • βœ… Android (API 24+, Android 7.0 Nougat and above)
  • βœ… iOS (iOS 14.1+)

Installation Instructions

πŸ“± Android Installation

Option 1: Install APK on Physical Device

  1. Download ledger-v1.0.1.apk from the Releases section
  2. Transfer the APK to your Android device
  3. Open the APK file and follow the installation prompts
  4. You may need to enable "Install from unknown sources" in Settings

Option 2: Android Virtual Device (AVD)

  1. Open Android Studio
  2. Go to Tools β†’ Device Manager β†’ Create Virtual Device
  3. Select a device (e.g., Pixel 6) and download a system image (API 24+)
  4. Start the emulator
  5. Drag and drop the APK file onto the emulator window
  6. The app will install and appear in the app drawer
🍎 iOS Installation (macOS only)
  1. Ensure you have Xcode 14.1+ installed
  2. Clone the repository:
    git clone https://git.uwaterloo.ca/d27dai/ledger.git
    cd ledger
  3. Build the iOS framework:
    ./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
  4. Open the iOS project:
    open iosApp/iosApp.xcodeproj
  5. Select your target device (Simulator or physical device)
  6. 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:

  1. Clone the repository
  2. Open in Android Studio/IntelliJ
  3. Wait for Gradle sync to complete
  4. Select composeApp configuration
  5. 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

Usage Guide

1. Account Creation & Login

  1. Launch the app - You'll see the landing screen with a video background
  2. Create Account - Tap "Create Account" to sign up with your phone number
  3. Verify OTP - Enter the 6-digit code sent to your phone
  4. Complete Profile - Enter your name and choose a unique username
  5. Permissions - Optionally enable notifications and camera access

2. Managing Friends

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

3. Creating & Tracking Debts

  1. Navigate to the Friends tab
  2. Tap on a friend to open the Ledger (expense conversation)
  3. Tap + to add a new expense
  4. Enter amount, description, and optionally attach a receipt
  5. The debt appears in both users' ledgers

4. Managing Groups

Groups allow multiple people to track shared expenses (e.g., roommates, trips):

  1. Go to the Groups tab (from Friends screen header)
  2. Tap Create Group β†’ Enter name and description
  3. Invite Members β†’ Search friends and add them
  4. Add Group Expenses β†’ Open group ledger and add shared costs
  5. View Balances β†’ See who owes what in the group

5. Receipts & Transactions

  • 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

πŸ“ Design Documents

Entity-Relationship Diagram

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"
Loading

UML Class Diagrams

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
Loading

🎯 Grading Instructions

Running the Application

The application is fully functional and can be run using the instructions in the Getting Started section.

Environment Configuration

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 path

The example file includes all required credentials:

  • SUPABASE_URL and SUPABASE_ANON_KEY for backend
  • OPENAI_API_KEY for receipt OCR

Database Configuration

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

Test Account

For grading purposes, you can create a new account using any valid phone number, or use the existing test flow:

  1. Launch the app
  2. Tap "Create Account"
  3. Enter a phone number (SMS verification will be sent)
  4. Complete the onboarding flow

Key Features to Test

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 & Test Commands

# Build Android APK
./gradlew :composeApp:assembleDebug

# Run unit tests
./gradlew :shared:testDebugUnitTest

# Install on connected device
./gradlew :composeApp:installDebug

Project Structure

ledger/
β”œβ”€β”€ 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

Additional Notes

  • 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

πŸ“š Additional Resources


πŸ“œ License

This project was created as part of CS 346 at the University of Waterloo.


Happy Tracking! πŸ’°

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Kotlin 92.8%
  • PLpgSQL 7.1%
  • Swift 0.1%