Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

jobs:
build:
name: Assemble Release APK
name: Build APK
runs-on: ubuntu-latest

steps:
Expand Down
25 changes: 12 additions & 13 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/src/main/java/lc/fungee/IngrediCheck/IngrediCheckApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.app.Application
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import lc.fungee.IngrediCheck.di.AppContainer
import com.posthog.PostHog
import lc.fungee.IngrediCheck.model.utils.AppConstants

class IngrediCheckApp : Application() {
lateinit var container: AppContainer
Expand Down Expand Up @@ -31,6 +33,8 @@ class IngrediCheckApp : Application() {
}

PostHogAndroid.setup(this, config)
val internal = AppConstants.isInternalEnabled(this)
PostHog.register("is_internal", internal)
}

companion object {
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,25 @@ object Analytics {
// Property key must match iOS exactly
PostHog.capture(event = "Image Captured", properties = mapOf("time" to epochSeconds))
}

fun identifyAndRegister(distinctId: String?, isInternal: Boolean, email: String? = null) {
if (!distinctId.isNullOrBlank()) {
val props = mutableMapOf<String, Any>("is_internal" to isInternal)
if (!email.isNullOrBlank()) props["email"] = email
PostHog.identify(
distinctId = distinctId,
userProperties = props
)
}
PostHog.register("is_internal", isInternal)
}

fun registerInternal(isInternal: Boolean) {
PostHog.register("is_internal", isInternal)
}

fun resetAndRegister(isInternal: Boolean) {
PostHog.reset()
PostHog.register("is_internal", isInternal)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lc.fungee.IngrediCheck.model.repository
package lc.fungee.IngrediCheck.model.repository
import lc.fungee.IngrediCheck.model.utils.AppConstants
import lc.fungee.IngrediCheck.model.entities.AppleAuthConfig

Expand All @@ -25,6 +25,7 @@ import lc.fungee.IngrediCheck.ui.view.screens.onboarding.AppleLoginWebViewActivi
import lc.fungee.IngrediCheck.model.source.SharedPreferencesSessionManager
import kotlin.time.ExperimentalTime
import lc.fungee.IngrediCheck.model.repository.auth.AuthProvider


@OptIn(ExperimentalTime::class)
class LoginAuthRepository(
Expand All @@ -44,6 +45,8 @@ class LoginAuthRepository(
install(Storage)
}



fun hasStoredSession(): Boolean {
return try {
context.getSharedPreferences(AppConstants.Prefs.SUPABASE_SESSION, Context.MODE_PRIVATE)
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ object AppConstants {
// SharedPreferences file names
const val USER_SESSION = "user_session"
const val SUPABASE_SESSION = "supabase_session"
const val INTERNAL_FLAGS = "internal_flags"

// Common keys inside SharedPreferences
const val KEY_LOGIN_PROVIDER = "login_provider"
const val KEY_DISCLAIMER_ACCEPTED = "disclaimer_accepted"
const val KEY_INTERNAL_MODE = "is_internal_user"
}

object Providers {
Expand Down Expand Up @@ -55,5 +57,21 @@ object AppConstants {
val HOST: String?
get() = Uri.parse(URL).host
}

fun isInternalEnabled(context: android.content.Context): Boolean {
return try {
context.getSharedPreferences(Prefs.INTERNAL_FLAGS, android.content.Context.MODE_PRIVATE)
.getBoolean(Prefs.KEY_INTERNAL_MODE, false)
} catch (_: Exception) { false }
}

fun setInternalEnabled(context: android.content.Context, enabled: Boolean) {
try {
context.getSharedPreferences(Prefs.INTERNAL_FLAGS, android.content.Context.MODE_PRIVATE)
.edit()
.putBoolean(Prefs.KEY_INTERNAL_MODE, enabled)
.apply()
} catch (_: Exception) { }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.TextButton
Expand Down Expand Up @@ -51,7 +52,9 @@ import lc.fungee.IngrediCheck.viewmodel.PreferenceViewModel
import lc.fungee.IngrediCheck.viewmodel.AppleAuthViewModel
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import kotlinx.coroutines.delay
import kotlinx.coroutines.Job
import lc.fungee.IngrediCheck.model.utils.AppConstants
import android.widget.Toast

enum class ConfirmAction {
NONE, DELETE_ACCOUNT, RESET_GUEST
Expand All @@ -75,7 +78,15 @@ fun SettingScreen(
val isGuest = loginProvider.isNullOrBlank() || loginProvider == AppConstants.Providers.ANONYMOUS
val coroutineScope = rememberCoroutineScope()
var showSignOutDialog by remember { mutableStateOf(false) }
var internalEnabled by remember { mutableStateOf(AppConstants.isInternalEnabled(context)) }
var versionTapCount by remember { mutableStateOf(0) }
var tapResetJob by remember { mutableStateOf<Job?>(null) }
var internalTapCount by remember { mutableStateOf(0) }
var internalTapResetJob by remember { mutableStateOf<Job?>(null) }
var isSignOutLoading by remember { mutableStateOf(false) }
var isResetLoading by remember { mutableStateOf(false) }
var showDeleteAccountDialog by remember { mutableStateOf(false) }
var isDeleteAccountLoading by remember { mutableStateOf(false) }
var showDeleteGuestDialog by remember { mutableStateOf(false) }
var confirmAction by remember { mutableStateOf(ConfirmAction.NONE) }
fun clearWebCookies() {
Expand Down Expand Up @@ -138,21 +149,32 @@ fun SettingScreen(
"Reset App State",
R.drawable.fluent_warning_20_regular,
tint = AppColors.ErrorStrong,
tint2 = AppColors.ErrorStrong, showDivider = false
tint2 = AppColors.ErrorStrong,
showDivider = false,
trailingLoading = isResetLoading
) { confirmAction = ConfirmAction.RESET_GUEST }
} else {
// Authenticated user: show Sign Out and Delete Data & Account
IconRow(
"Sign Out",
R.drawable.stash_signout_light__1_,
tint = AppColors.Brand, showArrow = false
) { clearAllSession()}
tint = AppColors.Brand,
showArrow = false,
trailingLoading = isSignOutLoading
) {
if (!isSignOutLoading) {
isSignOutLoading = true
clearAllSession()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Permanent Loading Blocks User Interaction

The loading states isSignOutLoading, isDeleteAccountLoading, and isResetLoading are set to true but never reset to false. If onRequireReauth() navigation fails or any operation errors occur before reaching it, the UI remains permanently stuck with a spinner and disabled buttons, preventing users from retrying the action.

Fix in Cursor Fix in Web

}
}
IconRow(
"Delete Data & Account",
R.drawable.fluent_warning_20_regular,
tint = AppColors.ErrorStrong,
tint2 = AppColors.ErrorStrong,showDivider = false
) { confirmAction = ConfirmAction.DELETE_ACCOUNT }
tint2 = AppColors.ErrorStrong,
showDivider = false,
trailingLoading = isDeleteAccountLoading
) { if (!isDeleteAccountLoading) confirmAction = ConfirmAction.DELETE_ACCOUNT }
}
}

Expand Down Expand Up @@ -190,11 +212,56 @@ fun SettingScreen(
// R.drawable.rightbackbutton
) { selectedUrl = AppConstants.Website.PRIVACY }

if (internalEnabled) {
IconRow(
"Internal Mode Enabled",
R.drawable.fluent_warning_20_regular,
tint = AppColors.Brand,
tint2 = AppColors.Brand,
showDivider = true,
showArrow = false,
onClick = {
internalTapCount += 1
if (internalTapCount == 1) {
internalTapResetJob?.cancel()
internalTapResetJob = coroutineScope.launch {
delay(1500)
internalTapCount = 0
}
}
if (internalTapCount >= 7) {
internalTapCount = 0
internalTapResetJob?.cancel()
viewModel.disableInternalMode(context)
internalEnabled = false
Toast.makeText(context, "Internal Mode Disabled", Toast.LENGTH_SHORT).show()
}
}
)
}

IconRow(
"IngrediCheck for Android 1.0.0.(4)",
R.drawable.rectangle_34624324__1_,
// null,
showDivider = false
showDivider = false,
onClick = {
versionTapCount += 1
if (versionTapCount == 1) {
tapResetJob?.cancel()
tapResetJob = coroutineScope.launch {
delay(1500)
versionTapCount = 0
}
}
if (versionTapCount >= 7) {
versionTapCount = 0
tapResetJob?.cancel()
viewModel.enableInternalMode(context)
internalEnabled = true
Toast.makeText(context, "Internal Mode Enabled", Toast.LENGTH_SHORT).show()
}
}
)
}

Expand All @@ -212,6 +279,7 @@ fun SettingScreen(
confirmText = "I Understand"
onConfirm = {
coroutineScope.launch {
isDeleteAccountLoading = true
// First: try remote account deletion (Edge Function must be deployed server-side)
runCatching { preferenceViewModel.deleteAccountRemote() }
clearAllSession()
Expand All @@ -223,6 +291,7 @@ fun SettingScreen(
confirmText = "I Understand"
onConfirm = {
coroutineScope.launch {
isResetLoading = true
runCatching { viewModel.signOut(context) }
runCatching { viewModel.clearSupabaseLocalSession() }
preferenceViewModel.clearAllLocalData()
Expand Down Expand Up @@ -515,6 +584,7 @@ private fun IconRow(
tint2: Color = AppColors.Brand,
showDivider: Boolean = true, //
showArrow: Boolean = true, // ✅ new flag
trailingLoading: Boolean = false,
onClick: (() -> Unit)? = null
) {
Column {
Expand All @@ -523,6 +593,7 @@ private fun IconRow(
.fillMaxWidth()
.height(45.dp)
.clickable(
enabled = !trailingLoading,
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onClick?.invoke() }
Expand All @@ -547,18 +618,19 @@ private fun IconRow(
)
)
Spacer(modifier = Modifier.weight(1f))
//
if(showDivider ) {
if(showArrow) {

Icon(
imageVector = Icons.Default.KeyboardArrowRight,
modifier = Modifier.size(20.dp),
contentDescription = null,
tint = Grey75
)
}

if (trailingLoading) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
color = AppColors.Brand
)
} else if (showDivider && showArrow) {
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
modifier = Modifier.size(20.dp),
contentDescription = null,
tint = Grey75
)
}
}

Expand Down
Loading