Skip to content

Commit 637cd84

Browse files
Internal mode improvement (#11)
* Add internal mode toggle and analytics integration Introduces internal mode toggling in settings, with tap gestures to enable/disable and persistent storage via SharedPreferences. Integrates internal mode state with analytics (PostHog) and updates user metadata in Supabase. Also adds loading indicators to settings actions and refactors analytics registration and user metadata update logic. * Call analytics update after Apple login success Apple authorization-code login now calls updateAnalyticsAndSupabase(session) on success. This aligns the Apple code flow with other sign-in paths (Google, Apple ID token). Result: PostHog identify + super property registration happens, and Supabase is_internal metadata is synced based on the stored flag. * Update user metadata using Supabase client Replaces the call to repository.updateUserMetadata with a direct call to repository.supabaseClient.auth.updateUser, updating the 'is_internal' field. Adds error handling to catch exceptions during the update. * Remove unused updateUserMetadata function Deleted the updateUserMetadata suspend function and related OkHttp imports from LoginAuthRepository as they are no longer used. This cleans up the repository and removes unnecessary dependencies. * Fix: Change job name to match required status check 'Build APK' * Fix Google sign-in analytics sync --------- Co-authored-by: justanotheratom <sanketpatel.1805@gmail.com>
1 parent da3975f commit 637cd84

8 files changed

Lines changed: 192 additions & 33 deletions

File tree

.github/workflows/build-apk.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ concurrency:
1414

1515
jobs:
1616
build:
17-
name: Assemble Release APK
17+
name: Build APK
1818
runs-on: ubuntu-latest
1919

2020
steps:

.idea/deploymentTargetSelector.xml

Lines changed: 12 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src/main/java/lc/fungee/IngrediCheck/IngrediCheckApp.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.app.Application
44
import com.posthog.android.PostHogAndroid
55
import com.posthog.android.PostHogAndroidConfig
66
import lc.fungee.IngrediCheck.di.AppContainer
7+
import com.posthog.PostHog
8+
import lc.fungee.IngrediCheck.model.utils.AppConstants
79

810
class IngrediCheckApp : Application() {
911
lateinit var container: AppContainer
@@ -31,6 +33,8 @@ class IngrediCheckApp : Application() {
3133
}
3234

3335
PostHogAndroid.setup(this, config)
36+
val internal = AppConstants.isInternalEnabled(this)
37+
PostHog.register("is_internal", internal)
3438
}
3539

3640
companion object {

app/src/main/java/lc/fungee/IngrediCheck/analytics/Analytics.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,25 @@ object Analytics {
151151
// Property key must match iOS exactly
152152
PostHog.capture(event = "Image Captured", properties = mapOf("time" to epochSeconds))
153153
}
154+
155+
fun identifyAndRegister(distinctId: String?, isInternal: Boolean, email: String? = null) {
156+
if (!distinctId.isNullOrBlank()) {
157+
val props = mutableMapOf<String, Any>("is_internal" to isInternal)
158+
if (!email.isNullOrBlank()) props["email"] = email
159+
PostHog.identify(
160+
distinctId = distinctId,
161+
userProperties = props
162+
)
163+
}
164+
PostHog.register("is_internal", isInternal)
165+
}
166+
167+
fun registerInternal(isInternal: Boolean) {
168+
PostHog.register("is_internal", isInternal)
169+
}
170+
171+
fun resetAndRegister(isInternal: Boolean) {
172+
PostHog.reset()
173+
PostHog.register("is_internal", isInternal)
174+
}
154175
}

app/src/main/java/lc/fungee/IngrediCheck/model/repository/LoginAuthRepository.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package lc.fungee.IngrediCheck.model.repository
1+
package lc.fungee.IngrediCheck.model.repository
22
import lc.fungee.IngrediCheck.model.utils.AppConstants
33
import lc.fungee.IngrediCheck.model.entities.AppleAuthConfig
44

@@ -25,6 +25,7 @@ import lc.fungee.IngrediCheck.ui.view.screens.onboarding.AppleLoginWebViewActivi
2525
import lc.fungee.IngrediCheck.model.source.SharedPreferencesSessionManager
2626
import kotlin.time.ExperimentalTime
2727
import lc.fungee.IngrediCheck.model.repository.auth.AuthProvider
28+
2829

2930
@OptIn(ExperimentalTime::class)
3031
class LoginAuthRepository(
@@ -44,6 +45,8 @@ class LoginAuthRepository(
4445
install(Storage)
4546
}
4647

48+
49+
4750
fun hasStoredSession(): Boolean {
4851
return try {
4952
context.getSharedPreferences(AppConstants.Prefs.SUPABASE_SESSION, Context.MODE_PRIVATE)

app/src/main/java/lc/fungee/IngrediCheck/model/utils/Constants.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ object AppConstants {
2323
// SharedPreferences file names
2424
const val USER_SESSION = "user_session"
2525
const val SUPABASE_SESSION = "supabase_session"
26+
const val INTERNAL_FLAGS = "internal_flags"
2627

2728
// Common keys inside SharedPreferences
2829
const val KEY_LOGIN_PROVIDER = "login_provider"
2930
const val KEY_DISCLAIMER_ACCEPTED = "disclaimer_accepted"
31+
const val KEY_INTERNAL_MODE = "is_internal_user"
3032
}
3133

3234
object Providers {
@@ -55,5 +57,21 @@ object AppConstants {
5557
val HOST: String?
5658
get() = Uri.parse(URL).host
5759
}
60+
61+
fun isInternalEnabled(context: android.content.Context): Boolean {
62+
return try {
63+
context.getSharedPreferences(Prefs.INTERNAL_FLAGS, android.content.Context.MODE_PRIVATE)
64+
.getBoolean(Prefs.KEY_INTERNAL_MODE, false)
65+
} catch (_: Exception) { false }
66+
}
67+
68+
fun setInternalEnabled(context: android.content.Context, enabled: Boolean) {
69+
try {
70+
context.getSharedPreferences(Prefs.INTERNAL_FLAGS, android.content.Context.MODE_PRIVATE)
71+
.edit()
72+
.putBoolean(Prefs.KEY_INTERNAL_MODE, enabled)
73+
.apply()
74+
} catch (_: Exception) { }
75+
}
5876
}
5977

app/src/main/java/lc/fungee/IngrediCheck/ui/view/screens/setting/Settingscreen.kt

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Lock
1919
import androidx.compose.material.icons.filled.Star
2020
import androidx.compose.material3.ExperimentalMaterial3Api
2121
import androidx.compose.material3.AlertDialog
22+
import androidx.compose.material3.CircularProgressIndicator
2223
import androidx.compose.material3.Icon
2324
import androidx.compose.material3.ModalBottomSheet
2425
import androidx.compose.material3.TextButton
@@ -51,7 +52,9 @@ import lc.fungee.IngrediCheck.viewmodel.PreferenceViewModel
5152
import lc.fungee.IngrediCheck.viewmodel.AppleAuthViewModel
5253
import com.google.android.gms.auth.api.signin.GoogleSignInClient
5354
import kotlinx.coroutines.delay
55+
import kotlinx.coroutines.Job
5456
import lc.fungee.IngrediCheck.model.utils.AppConstants
57+
import android.widget.Toast
5558

5659
enum class ConfirmAction {
5760
NONE, DELETE_ACCOUNT, RESET_GUEST
@@ -75,7 +78,15 @@ fun SettingScreen(
7578
val isGuest = loginProvider.isNullOrBlank() || loginProvider == AppConstants.Providers.ANONYMOUS
7679
val coroutineScope = rememberCoroutineScope()
7780
var showSignOutDialog by remember { mutableStateOf(false) }
81+
var internalEnabled by remember { mutableStateOf(AppConstants.isInternalEnabled(context)) }
82+
var versionTapCount by remember { mutableStateOf(0) }
83+
var tapResetJob by remember { mutableStateOf<Job?>(null) }
84+
var internalTapCount by remember { mutableStateOf(0) }
85+
var internalTapResetJob by remember { mutableStateOf<Job?>(null) }
86+
var isSignOutLoading by remember { mutableStateOf(false) }
87+
var isResetLoading by remember { mutableStateOf(false) }
7888
var showDeleteAccountDialog by remember { mutableStateOf(false) }
89+
var isDeleteAccountLoading by remember { mutableStateOf(false) }
7990
var showDeleteGuestDialog by remember { mutableStateOf(false) }
8091
var confirmAction by remember { mutableStateOf(ConfirmAction.NONE) }
8192
fun clearWebCookies() {
@@ -138,21 +149,32 @@ fun SettingScreen(
138149
"Reset App State",
139150
R.drawable.fluent_warning_20_regular,
140151
tint = AppColors.ErrorStrong,
141-
tint2 = AppColors.ErrorStrong, showDivider = false
152+
tint2 = AppColors.ErrorStrong,
153+
showDivider = false,
154+
trailingLoading = isResetLoading
142155
) { confirmAction = ConfirmAction.RESET_GUEST }
143156
} else {
144157
// Authenticated user: show Sign Out and Delete Data & Account
145158
IconRow(
146159
"Sign Out",
147160
R.drawable.stash_signout_light__1_,
148-
tint = AppColors.Brand, showArrow = false
149-
) { clearAllSession()}
161+
tint = AppColors.Brand,
162+
showArrow = false,
163+
trailingLoading = isSignOutLoading
164+
) {
165+
if (!isSignOutLoading) {
166+
isSignOutLoading = true
167+
clearAllSession()
168+
}
169+
}
150170
IconRow(
151171
"Delete Data & Account",
152172
R.drawable.fluent_warning_20_regular,
153173
tint = AppColors.ErrorStrong,
154-
tint2 = AppColors.ErrorStrong,showDivider = false
155-
) { confirmAction = ConfirmAction.DELETE_ACCOUNT }
174+
tint2 = AppColors.ErrorStrong,
175+
showDivider = false,
176+
trailingLoading = isDeleteAccountLoading
177+
) { if (!isDeleteAccountLoading) confirmAction = ConfirmAction.DELETE_ACCOUNT }
156178
}
157179
}
158180

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

215+
if (internalEnabled) {
216+
IconRow(
217+
"Internal Mode Enabled",
218+
R.drawable.fluent_warning_20_regular,
219+
tint = AppColors.Brand,
220+
tint2 = AppColors.Brand,
221+
showDivider = true,
222+
showArrow = false,
223+
onClick = {
224+
internalTapCount += 1
225+
if (internalTapCount == 1) {
226+
internalTapResetJob?.cancel()
227+
internalTapResetJob = coroutineScope.launch {
228+
delay(1500)
229+
internalTapCount = 0
230+
}
231+
}
232+
if (internalTapCount >= 7) {
233+
internalTapCount = 0
234+
internalTapResetJob?.cancel()
235+
viewModel.disableInternalMode(context)
236+
internalEnabled = false
237+
Toast.makeText(context, "Internal Mode Disabled", Toast.LENGTH_SHORT).show()
238+
}
239+
}
240+
)
241+
}
242+
193243
IconRow(
194244
"IngrediCheck for Android 1.0.0.(4)",
195245
R.drawable.rectangle_34624324__1_,
196246
// null,
197-
showDivider = false
247+
showDivider = false,
248+
onClick = {
249+
versionTapCount += 1
250+
if (versionTapCount == 1) {
251+
tapResetJob?.cancel()
252+
tapResetJob = coroutineScope.launch {
253+
delay(1500)
254+
versionTapCount = 0
255+
}
256+
}
257+
if (versionTapCount >= 7) {
258+
versionTapCount = 0
259+
tapResetJob?.cancel()
260+
viewModel.enableInternalMode(context)
261+
internalEnabled = true
262+
Toast.makeText(context, "Internal Mode Enabled", Toast.LENGTH_SHORT).show()
263+
}
264+
}
198265
)
199266
}
200267

@@ -212,6 +279,7 @@ fun SettingScreen(
212279
confirmText = "I Understand"
213280
onConfirm = {
214281
coroutineScope.launch {
282+
isDeleteAccountLoading = true
215283
// First: try remote account deletion (Edge Function must be deployed server-side)
216284
runCatching { preferenceViewModel.deleteAccountRemote() }
217285
clearAllSession()
@@ -223,6 +291,7 @@ fun SettingScreen(
223291
confirmText = "I Understand"
224292
onConfirm = {
225293
coroutineScope.launch {
294+
isResetLoading = true
226295
runCatching { viewModel.signOut(context) }
227296
runCatching { viewModel.clearSupabaseLocalSession() }
228297
preferenceViewModel.clearAllLocalData()
@@ -515,6 +584,7 @@ private fun IconRow(
515584
tint2: Color = AppColors.Brand,
516585
showDivider: Boolean = true, //
517586
showArrow: Boolean = true, // ✅ new flag
587+
trailingLoading: Boolean = false,
518588
onClick: (() -> Unit)? = null
519589
) {
520590
Column {
@@ -523,6 +593,7 @@ private fun IconRow(
523593
.fillMaxWidth()
524594
.height(45.dp)
525595
.clickable(
596+
enabled = !trailingLoading,
526597
indication = null,
527598
interactionSource = remember { MutableInteractionSource() }
528599
) { onClick?.invoke() }
@@ -547,18 +618,19 @@ private fun IconRow(
547618
)
548619
)
549620
Spacer(modifier = Modifier.weight(1f))
550-
//
551-
if(showDivider ) {
552-
if(showArrow) {
553-
554-
Icon(
555-
imageVector = Icons.Default.KeyboardArrowRight,
556-
modifier = Modifier.size(20.dp),
557-
contentDescription = null,
558-
tint = Grey75
559-
)
560-
}
561-
621+
if (trailingLoading) {
622+
CircularProgressIndicator(
623+
modifier = Modifier.size(18.dp),
624+
strokeWidth = 2.dp,
625+
color = AppColors.Brand
626+
)
627+
} else if (showDivider && showArrow) {
628+
Icon(
629+
imageVector = Icons.Default.KeyboardArrowRight,
630+
modifier = Modifier.size(20.dp),
631+
contentDescription = null,
632+
tint = Grey75
633+
)
562634
}
563635
}
564636

0 commit comments

Comments
 (0)