Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.gradle
/build
app/build
*.iml
.idea
local.properties
.DS_Store
Binary file added .gradle/8.8/checksums/checksums.lock
Binary file not shown.
Binary file added .gradle/8.8/checksums/md5-checksums.bin
Binary file not shown.
Binary file added .gradle/8.8/checksums/sha1-checksums.bin
Binary file not shown.
File renamed without changes.
Binary file added .gradle/8.8/executionHistory/executionHistory.bin
Binary file not shown.
Binary file added .gradle/8.8/executionHistory/executionHistory.lock
Binary file not shown.
Binary file added .gradle/8.8/fileChanges/last-build.bin
Binary file not shown.
Binary file added .gradle/8.8/fileHashes/fileHashes.bin
Binary file not shown.
Binary file added .gradle/8.8/fileHashes/fileHashes.lock
Binary file not shown.
Binary file added .gradle/8.8/fileHashes/resourceHashesCache.bin
Binary file not shown.
File renamed without changes.
Binary file added .gradle/buildOutputCleanup/buildOutputCleanup.lock
Binary file not shown.
2 changes: 2 additions & 0 deletions .gradle/buildOutputCleanup/cache.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#Wed Feb 25 09:17:28 UTC 2026
gradle.version=8.8
Binary file added .gradle/buildOutputCleanup/outputFiles.bin
Binary file not shown.
File renamed without changes.
98 changes: 98 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}

android {
namespace = "com.fairslice"
compileSdk = 34

defaultConfig {
applicationId = "com.fairslice"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.10"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
val lifecycle_version = "2.7.0"
val room_version = "2.6.1"
val hilt_version = "2.50"
val nav_version = "2.7.7"
val work_version = "2.9.0"

implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")

implementation("androidx.navigation:navigation-compose:$nav_version")

implementation("com.google.dagger:hilt-android:$hilt_version")
ksp("com.google.dagger:hilt-android-compiler:$hilt_version")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
implementation("androidx.hilt:hilt-work:1.2.0")
ksp("androidx.hilt:hilt-compiler:1.2.0")

implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version")
ksp("androidx.room:room-compiler:$room_version")

implementation("androidx.work:work-runtime-ktx:$work_version")

implementation("io.coil-kt:coil-compose:2.5.0")
implementation("androidx.compose.ui:ui-text-google-fonts:1.6.2")
implementation("com.airbnb.android:lottie-compose:6.3.0")
implementation("nl.dionsegijn:konfetti-compose:2.0.4")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
25 changes: 25 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".FairSliceApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="FairSlice"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FairSlice">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.FairSlice">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
13 changes: 13 additions & 0 deletions app/src/main/kotlin/com/fairslice/FairSliceApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.fairslice

import android.app.Application
import com.fairslice.utils.NotificationHelper
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class FairSliceApp : Application() {
override fun onCreate() {
super.onCreate()
NotificationHelper.createNotificationChannel(this)
}
}
141 changes: 141 additions & 0 deletions app/src/main/kotlin/com/fairslice/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.fairslice

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.fairslice.ui.dashboard.DashboardScreen
import com.fairslice.ui.onboarding.OnboardingScreen
import com.fairslice.ui.newsplit.AddPeopleScreen
import com.fairslice.ui.history.HistoryScreen
import com.fairslice.ui.newsplit.NewSplitScreen
import com.fairslice.ui.newsplit.NewSplitViewModel
import com.fairslice.ui.results.ResultsScreen
import com.fairslice.ui.settings.SettingsScreen
import com.fairslice.ui.navigation.Screen
import com.fairslice.ui.theme.FairSliceTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
FairSliceTheme {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
// Handle result if needed
}

LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}

Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
FairSliceNavigation()
}
}
}
}
}

@Composable
fun FairSliceNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Onboarding.route) {
composable(Screen.Onboarding.route) {
OnboardingScreen(
onFinished = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Onboarding.route) { inclusive = true }
}
}
)
}
composable(Screen.Dashboard.route) {
DashboardScreen(
onNavigateToNewSplit = { navController.navigate(Screen.NewSplit.route) },
onNavigateToHistory = { navController.navigate(Screen.History.route) },
onNavigateToSettings = { navController.navigate(Screen.Settings.route) }
)
}
composable(Screen.NewSplit.route) {
val viewModel: NewSplitViewModel = hiltViewModel()
NewSplitScreen(
onNext = { name, amount, category ->
navController.navigate(Screen.AddPeople.createRoute(name, amount.toDouble(), category))
},
viewModel = viewModel
)
}
composable(
route = Screen.AddPeople.route,
arguments = listOf(
navArgument("billName") { type = NavType.StringType },
navArgument("totalAmount") { type = NavType.FloatType },
navArgument("category") { type = NavType.StringType }
)
) {
val viewModel: NewSplitViewModel = hiltViewModel() // In a real app, we'd share this
AddPeopleScreen(
onResults = { billId ->
navController.navigate(Screen.Results.createRoute(billId)) {
popUpTo(Screen.Dashboard.route)
}
},
viewModel = viewModel
)
}
composable(
route = Screen.Results.route,
arguments = listOf(navArgument("billId") { type = NavType.LongType })
) {
ResultsScreen(
onDone = { navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
} }
)
}
composable(Screen.History.route) {
HistoryScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onBack = { navController.popBackStack() }
)
}
}
}
49 changes: 49 additions & 0 deletions app/src/main/kotlin/com/fairslice/data/local/FairSliceDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.fairslice.data.local

import androidx.room.*
import com.fairslice.data.models.Bill
import com.fairslice.data.models.Participant
import com.fairslice.data.models.UserProfile
import kotlinx.coroutines.flow.Flow

@Dao
interface FairSliceDao {
@Query("SELECT * FROM bills ORDER BY date DESC")
fun getAllBills(): Flow<List<Bill>>

@Insert
suspend fun insertBill(bill: Bill): Long

@Query("SELECT * FROM participants WHERE billId = :billId")
suspend fun getParticipantsForBill(billId: Long): List<Participant>

@Insert
suspend fun insertParticipants(participants: List<Participant>)

@Query("SELECT * FROM user_profile LIMIT 1")
fun getUserProfile(): Flow<UserProfile?>

@Upsert
suspend fun upsertUserProfile(profile: UserProfile)

@Transaction
suspend fun insertBillWithParticipants(bill: Bill, participants: List<Participant>): Long {
val billId = insertBill(bill)
val participantsWithId = participants.map { it.copy(billId = billId) }
insertParticipants(participantsWithId)
return billId
}

@Transaction
@Query("SELECT * FROM bills WHERE id = :billId")
suspend fun getBillWithParticipants(billId: Long): BillWithParticipants
}

data class BillWithParticipants(
@Embedded val bill: Bill,
@Relation(
parentColumn = "id",
entityColumn = "billId"
)
val participants: List<Participant>
)
12 changes: 12 additions & 0 deletions app/src/main/kotlin/com/fairslice/data/local/FairSliceDatabase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.fairslice.data.local

import androidx.room.Database
import androidx.room.RoomDatabase
import com.fairslice.data.models.Bill
import com.fairslice.data.models.Participant
import com.fairslice.data.models.UserProfile

@Database(entities = [Bill::class, Participant::class, UserProfile::class], version = 1)
abstract class FairSliceDatabase : RoomDatabase() {
abstract fun dao(): FairSliceDao
}
33 changes: 33 additions & 0 deletions app/src/main/kotlin/com/fairslice/data/models/Entities.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.fairslice.data.models

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "bills")
data class Bill(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val totalAmountCents: Long,
val category: String,
val date: Long = System.currentTimeMillis(),
val isPaid: Boolean = false
)

@Entity(tableName = "participants")
data class Participant(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val billId: Long,
val name: String,
val income: Double, // Income can stay Double as it's often large/normalized
val incomeType: String, // ANNUAL, MONTHLY, HOURLY
val amountOwedCents: Long = 0,
val isPaid: Boolean = false
)

@Entity(tableName = "user_profile")
data class UserProfile(
@PrimaryKey val id: Int = 1,
val name: String,
val annualIncome: Double,
val currency: String = "USD"
)
Loading