Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
a885feb
Initial login refactor
caleb-bit Oct 15, 2025
6e81124
Merge branch 'main' into Caleb/LoginRefactor
caleb-bit Oct 26, 2025
526b824
Merge branch 'main' of https://github.com/cuappdev/eatery-blue-androi…
caleb-bit Oct 29, 2025
900f4b5
Initial login route refactor
caleb-bit Nov 5, 2025
890cafa
Merge branch 'main' of https://github.com/cuappdev/eatery-blue-androi…
caleb-bit Nov 5, 2025
1e1529d
Remove netID from settings
caleb-bit Nov 8, 2025
44a3e15
Implement transactions retrieval plus internal refactoring
caleb-bit Nov 9, 2025
3e6528a
Fix exposed loadedUser variable, use firstOrNull, plus other minor fi…
caleb-bit Nov 19, 2025
8aebb23
Initial migration to new backend
caleb-bit Feb 18, 2026
1aff093
Remove no longer used buildConfigFields
caleb-bit Feb 26, 2026
306a021
Fix financials and login routes and use flows for account page
caleb-bit Mar 2, 2026
810228c
Fix time zone issue with transaction times
caleb-bit Mar 2, 2026
b95817e
Keep user logged in
caleb-bit Mar 2, 2026
00eb765
Fix favoriting routes
caleb-bit Mar 2, 2026
06c3021
Complete favoriting logic
caleb-bit Mar 2, 2026
60f3e33
Fix logout behavior
caleb-bit Mar 2, 2026
b756317
Fix meal filter formatting
caleb-bit Mar 2, 2026
b5af0f4
Fix capitalized meal times in details screen
caleb-bit Mar 4, 2026
c995b42
Improve code quality in Eatery
caleb-bit Mar 4, 2026
a99f6cd
Refresh favorites when HomeScreen opens
caleb-bit Mar 4, 2026
4928943
Implement in-app updates
caleb-bit Mar 5, 2026
edb57ac
Clean up comments
caleb-bit Mar 5, 2026
8c33e01
Clean up logging
caleb-bit Mar 7, 2026
33c51da
Merge pull request #206 from cuappdev/Caleb/inAppUpdate
caleb-bit Mar 7, 2026
4cba7b2
Revert favoriting to local
caleb-bit Mar 7, 2026
d13617c
Merge pull request #207 from cuappdev/Caleb/revert-favoriting
caleb-bit Mar 7, 2026
fb20b81
Implement error handling system for networking calls
caleb-bit Mar 7, 2026
bd9063e
Implement toast error handling
caleb-bit Mar 7, 2026
7bf04e8
Initial plan
Copilot Mar 7, 2026
9f18541
Fix: only call getFinancials() when linkGETAccount succeeds
Copilot Mar 7, 2026
60fb166
Merge pull request #208 from cuappdev/copilot/sub-pr-203
caleb-bit Mar 7, 2026
cb7a73b
Remove redundant UUID creation
caleb-bit Mar 7, 2026
42d0f9f
Make LoginViewModel correctly observe userRepository
caleb-bit Mar 7, 2026
38016cb
Create VM for settings
caleb-bit Mar 7, 2026
a4cd589
Fix issue with menuItemsToEateries
caleb-bit Mar 7, 2026
fc0be18
Use safer calls
caleb-bit Mar 7, 2026
e8c2e95
Fix incorrect documentation
caleb-bit Mar 7, 2026
dbbc137
Initial plan
Copilot Mar 7, 2026
23dfd55
Fix memory leak: store and unregister flexible update listener in Mai…
Copilot Mar 7, 2026
dd37573
Remove redundant preview
caleb-bit Mar 7, 2026
69ac9aa
Minor code improvements to LoginViewModel
caleb-bit Mar 7, 2026
17c11fe
Extract unregistering listener logic
caleb-bit Mar 7, 2026
78c1f03
Merge pull request #209 from cuappdev/copilot/sub-pr-203
caleb-bit Mar 7, 2026
7bc2278
Remove runBlocking call
caleb-bit Mar 8, 2026
1987c22
Improve redundant computation
caleb-bit Mar 8, 2026
e5c5ce3
Use null-safe access
caleb-bit Mar 8, 2026
892c76d
Remove debugging variable
caleb-bit Mar 8, 2026
9cf0dc7
Reuse function
caleb-bit Mar 8, 2026
c319240
Fix visibility issue
caleb-bit Mar 8, 2026
8f72059
Fix typo
caleb-bit Mar 8, 2026
84717d4
Use setPref helper
caleb-bit Mar 8, 2026
fb6cf94
Fix import issue
caleb-bit Mar 8, 2026
d977e0c
Merge
caleb-bit Mar 8, 2026
4629308
Simplify function
caleb-bit Mar 8, 2026
3115631
Import UUID
caleb-bit Mar 8, 2026
eed01f9
Use flows in UserPreferencesRepository
caleb-bit Mar 8, 2026
b78ecef
Fix name typo
caleb-bit Mar 8, 2026
fe03d2c
Simplify collection transformation
caleb-bit Mar 8, 2026
70690dd
Clean up EateryMenusBottomSheet.kt
caleb-bit Mar 8, 2026
4af08a1
Add preview for EateryDetailScreen
caleb-bit Mar 8, 2026
f281e01
Prevent race condition from getDeviceId
caleb-bit Mar 8, 2026
61becf2
Use collectAsStateWithLifecycle
caleb-bit Mar 8, 2026
7943032
Unregister listener in onStop
caleb-bit Mar 8, 2026
8a25535
Improve flow practice
caleb-bit Mar 8, 2026
feead08
Add limit count and duplicate checking to recent searches
caleb-bit Mar 8, 2026
8ca48fe
Extract AuthTokenRepository from UserRepository
caleb-bit Mar 8, 2026
bcb87c7
Remove redundant comments
caleb-bit Mar 8, 2026
116c7b2
Remove one-item column
caleb-bit Mar 8, 2026
4be93e9
Remove duplicate statusBarPadding
caleb-bit Mar 8, 2026
b89f021
Fix index issue
caleb-bit Mar 8, 2026
17168e8
Add preview for EateryMenusBottomSheet
caleb-bit Mar 9, 2026
3101ba4
Replace spacer with divider
caleb-bit Mar 9, 2026
1d12c4f
Hoist filterText
caleb-bit Mar 9, 2026
daefb4b
Add key for lazycolumn
caleb-bit Mar 9, 2026
c63bb3b
Replace spacers with dividers
caleb-bit Mar 9, 2026
2fa4e39
Hoist date formatting logic
caleb-bit Mar 9, 2026
5ae05e6
Fix inefficient recomputation
caleb-bit Mar 9, 2026
3b52440
Prevent passing state down
caleb-bit Mar 9, 2026
275a709
Use firstOrNull() instead of first()
caleb-bit Mar 9, 2026
aba118d
Fix logout behavior
caleb-bit Mar 9, 2026
6ea6e01
Initial plan
Copilot Mar 9, 2026
4b3725d
Encrypt sensitive auth credentials stored in DataStore at rest
Copilot Mar 9, 2026
8e51104
Implement OkHttp interceptor
caleb-bit Mar 9, 2026
aca978c
Extract functionality
caleb-bit Mar 9, 2026
4ebe6e5
Remove unnecessary legacy handling
caleb-bit Mar 9, 2026
8ae1fe7
Ensure backward compatibility in user_prefs
caleb-bit Mar 9, 2026
ead488c
Merge branch 'Caleb/LoginRefactor' of https://github.com/cuappdev/eat…
caleb-bit Mar 9, 2026
93edb40
Merge pull request #210 from cuappdev/copilot/sub-pr-203
caleb-bit Mar 9, 2026
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
8 changes: 6 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ android {
useSupportLibrary true
}

buildConfigField("String", "GET_BACKEND_URL", secretsProperties['GET_BACKEND_URL'])
buildConfigField("String", "SESSIONID_WEBVIEW_URL", secretsProperties['SESSIONID_WEBVIEW_URL'])
buildConfigField("String", "CORNELL_INSTITUTION_ID", secretsProperties['CORNELL_INSTITUTION_ID'])
buildConfigField("boolean", "USE_LOCAL_FAVORITES", "true")
}

buildTypes {
Expand Down Expand Up @@ -105,6 +104,7 @@ dependencies {
// Networking
implementation("com.squareup.moshi:moshi:1.14.0")
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
implementation 'com.squareup.moshi:moshi-adapters:1.14.0'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.10'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
Expand Down Expand Up @@ -144,6 +144,10 @@ dependencies {
implementation 'com.valentinilk.shimmer:compose-shimmer:1.0.3'

implementation "io.coil-kt:coil-compose:2.2.2"

// In-app Updates
implementation("com.google.android.play:app-update:2.1.0")
implementation("com.google.android.play:app-update-ktx:2.1.0")
}

protobuf {
Expand Down
119 changes: 113 additions & 6 deletions app/src/main/java/com/cornellappdev/android/eatery/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,52 @@ package com.cornellappdev.android.eatery
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.WindowCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.cornellappdev.android.eatery.data.repositories.AuthTokenRepository
import com.cornellappdev.android.eatery.data.repositories.EateryRepository
import com.cornellappdev.android.eatery.data.repositories.UserPreferencesRepository
import com.cornellappdev.android.eatery.data.repositories.UserRepository
import com.cornellappdev.android.eatery.ui.navigation.NavigationSetup
import com.cornellappdev.android.eatery.util.LockScreenOrientation
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var userPreferences: UserPreferencesRepository
lateinit var eateryRepository: EateryRepository

@Inject
lateinit var eateryRepository: EateryRepository
lateinit var userRepository: UserRepository

@Inject
lateinit var authTokenRepository: AuthTokenRepository

private lateinit var activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>
private val appUpdateManager by lazy { AppUpdateManagerFactory.create(applicationContext) }
private var flexibleUpdateListener: InstallStateUpdatedListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val hasOnboarded = runBlocking {
return@runBlocking userPreferences.getHasOnboarded()
}
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) {}

val hasOnboarded = runBlocking { userRepository.hasOnboarded() }
Comment thread
caleb-bit marked this conversation as resolved.

WindowCompat.setDecorFitsSystemWindows(window, false)

Expand All @@ -46,5 +67,91 @@ class MainActivity : ComponentActivity() {
}
}
lifecycle.addObserver(dataRefresher)
checkForUpdateAvailability()
lifecycleScope.launch {
configureTokens()
userRepository.updateFavorites()
authTokenRepository.markTokensAsConfigured()
}
}

override fun onStop() {
super.onStop()
unregisterFlexibleUpdateListener()
}

override fun onDestroy() {
Comment thread
caleb-bit marked this conversation as resolved.
super.onDestroy()
// in case onStop cleanup did not run
unregisterFlexibleUpdateListener()
}

override fun onResume() {
super.onResume()

// Check if there's an update that's already downloaded and waiting to be installed
appUpdateManager
.appUpdateInfo
.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// If an in-app update is already running, resume the update.
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
activityResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}

if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
// If the update is downloaded but not installed, notify the user to complete the update.
appUpdateManager.completeUpdate()
}
}
}

private fun checkForUpdateAvailability() {
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
val isImmediateUpdateAllowed =
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
val isFlexibleUpdateAllowed =
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)

// For high priority updates, use immediate update
if (appUpdateInfo.updatePriority() >= 4 && isImmediateUpdateAllowed) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
activityResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
// For normal priority updates, use flexible update
else if (isFlexibleUpdateAllowed) {
// Create a listener to track flexible update progress
flexibleUpdateListener = InstallStateUpdatedListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
unregisterFlexibleUpdateListener()
appUpdateManager.completeUpdate()
}
}
flexibleUpdateListener?.let { appUpdateManager.registerListener(it) }

appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
activityResultLauncher,
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}
}
}
}

private fun unregisterFlexibleUpdateListener() {
flexibleUpdateListener?.let(appUpdateManager::unregisterListener)
flexibleUpdateListener = null
}

private suspend fun configureTokens() {
authTokenRepository.getTokens()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.cornellappdev.android.eatery.data

import com.cornellappdev.android.eatery.data.repositories.AuthTokenRepository
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Provider

/**
* OkHttp interceptor that automatically adds Bearer token to authenticated requests.
* Also handles token refresh on 401 responses.
*
* Uses Provider<AuthTokenRepository> to avoid circular dependency
*/
class AuthInterceptor @Inject constructor(
private val authTokenRepositoryProvider: Provider<AuthTokenRepository>
) : Interceptor {

companion object {
private val PUBLIC_ENDPOINTS = setOf(
"/eateries/",
"/auth/verify-token",
"/auth/refresh-token"
)
}

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

if (!isPublicEndpoint(request)) {
val requestWithToken = addTokenToRequest(request)
var response = chain.proceed(requestWithToken)

if (response.code == 401) {
response.close()
try {
runBlocking {
authTokenRepositoryProvider.get().refreshTokens()
}
val retryRequest = addTokenToRequest(request)
response = chain.proceed(retryRequest)
} catch (_: Exception) {
return chain.proceed(request)
}
}

return response
}

return chain.proceed(request)
}

private fun addTokenToRequest(request: Request): Request {
return try {
val token = runBlocking {
authTokenRepositoryProvider.get().getAccessToken()
}
request.newBuilder()
.header("Authorization", token)
.build()
} catch (_: Exception) {
request
}
}

private fun isPublicEndpoint(request: Request): Boolean {
val path = request.url.encodedPath
return PUBLIC_ENDPOINTS.any { path.startsWith(it) }
}
}

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("unused")

package com.cornellappdev.android.eatery.data

import com.cornellappdev.android.eatery.data.models.AccountType
Expand Down Expand Up @@ -62,37 +64,15 @@ class ReportAdapter {
}
}

class TransactionDateAdapter {
@FromJson
fun fromJson(date: String): Date {
try {
val simpleDate =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ", Locale.US).parse(date)
if (simpleDate != null) {
return simpleDate
}
} catch (e: ParseException) {
e.printStackTrace()
}
return Date(0)
}
}

class DateTimeAdapter {
@ToJson
fun toJson(dateTime: LocalDateTime): String {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"))
}

@FromJson
fun fromJson(dateTime: Long): LocalDateTime {
try {
val instant = Instant.ofEpochSecond(dateTime)
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
} catch (e: ParseException) {
e.printStackTrace()
}
return LocalDateTime.MIN
fun fromJson(dateTime: String): LocalDateTime {
return LocalDateTime.ofInstant(Instant.parse(dateTime), ZoneId.systemDefault())
}
}

Expand All @@ -116,18 +96,14 @@ class AccountTypeAdapter {
"brb"
}

AccountType.CITYBUCKS -> {
AccountType.CITY_BUCKS -> {
"city bucks"
}

AccountType.LAUNDRY -> {
"laundry"
}

AccountType.MEALSWIPES -> {
"meal plan"
}

else -> {
"other"
}
Expand Down Expand Up @@ -155,7 +131,7 @@ class AccountTypeAdapter {
return if (accountName.contains("brb", ignoreCase = true)) {
AccountType.BRBS
} else if (accountName.contains("city bucks", ignoreCase = true)) {
AccountType.CITYBUCKS
AccountType.CITY_BUCKS
} else if (accountName.contains("laundry", ignoreCase = true)) {
AccountType.LAUNDRY
} else {
Expand Down
Loading
Loading