Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6736ff72 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix(auth): harden rate-limit, source hydration, and Play gating flows

parent 95902b5a
Loading
Loading
Loading
Loading
+10 −3
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@@ -58,6 +60,12 @@ class EnabledSourceState @Inject constructor(
    private val _enabledSourcesFlow = MutableStateFlow(emptySet<Source>())
    val enabledSourcesFlow: StateFlow<Set<Source>> = _enabledSourcesFlow.asStateFlow()
    private val _hasHydratedCapabilities = MutableStateFlow(false)
    val hydratedEnabledSourcesFlow: Flow<Set<Source>> =
        combine(enabledSourcesFlow, _hasHydratedCapabilities) { enabledSources, hasHydrated ->
            enabledSources.takeIf { hasHydrated }
        }
            .filterNotNull()
            .distinctUntilChanged()

    init {
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
@@ -130,8 +138,7 @@ class EnabledSourceState @Inject constructor(
}

internal fun EnabledSourceState.enabledStoreChanges(): Flow<Set<Source>> =
    enabledSourcesFlow
        .filter { hasHydratedCapabilities }
    hydratedEnabledSourcesFlow
        .drop(1)

internal fun Source.isAccessible(capabilities: UserCapabilities): Boolean {
+14 −2
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.isUnFiltered
import foundation.e.apps.data.handleNetworkResult
import foundation.e.apps.data.playstore.PlayStoreRepository
import javax.inject.Inject

class CategoryApiImpl @Inject constructor(
@@ -86,9 +87,14 @@ class CategoryApiImpl @Inject constructor(
    private suspend fun fetchGplayCategories(
        type: CategoryType,
    ): Pair<List<Category>, ResultStatus> {
        val playStoreRepository = awaitPlayStoreRepository()
            ?: return Pair(
                emptyList(),
                ResultStatus.UNKNOWN.apply { message = "Play Store unavailable" },
            )
        val categoryList = mutableListOf<Category>()
        val result = handleNetworkResult {
            val playResponse = appSources.gplayRepo.getCategories(type).map { gplayCategory ->
            val playResponse = playStoreRepository.getCategories(type).map { gplayCategory ->
                val category = gplayCategory.toCategory()
                category.drawable =
                    CategoryUtils.provideAppsCategoryIconResource(
@@ -154,9 +160,11 @@ class CategoryApiImpl @Inject constructor(
        category: String,
        pageUrl: String?
    ): ResultSupreme<Pair<List<Application>, String>> {
        val playStoreRepository = awaitPlayStoreRepository()
            ?: return ResultSupreme.Error("Play Store unavailable")
        return handleNetworkResult {
            val cluster =
                appSources.gplayRepo.getAppsByCategory(category, pageUrl)
                playStoreRepository.getAppsByCategory(category, pageUrl)

            val filteredApps = filterRestrictedGPlayApps(cluster.clusterAppList)
            val applications = (filteredApps.data ?: emptyList()).toMutableList()
@@ -232,4 +240,8 @@ class CategoryApiImpl @Inject constructor(

        else -> null
    }

    private suspend fun awaitPlayStoreRepository(): PlayStoreRepository? {
        return enabledStoreRepositoryProvider.awaitStore(Source.PLAY_STORE) as? PlayStoreRepository
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ class SignInFragment : Fragment() {
            },
            onError = ::showErrorDialog,
        )
        systemWideGoogleLoginLauncher.restore(savedInstanceState)
        requireActivity().onBackPressedDispatcher.addCallback(
            this,
            object : OnBackPressedCallback(true) {
@@ -118,6 +119,11 @@ class SignInFragment : Fragment() {
        dialogState = null
    }

    override fun onSaveInstanceState(outState: Bundle) {
        systemWideGoogleLoginLauncher.save(outState)
        super.onSaveInstanceState(outState)
    }

    private fun navigateToGoogleSignInFragment(): Boolean {
        return findNavController().safeNavigate(
            R.id.signInFragment,
+6 −0
Original line number Diff line number Diff line
@@ -20,6 +20,12 @@ package foundation.e.apps.feature.auth.login
internal class SystemWideGoogleLoginCoordinator {
    private var pendingAccountName: String? = null

    fun snapshotPendingAccountName(): String? = pendingAccountName

    fun restorePendingAccountName(accountName: String?) {
        pendingAccountName = accountName?.takeIf { it.isNotBlank() }
    }

    fun start(
        hasSupportedMicrog: Boolean,
        hasMicrogAccount: Boolean,
+25 −0
Original line number Diff line number Diff line
@@ -20,7 +20,9 @@ package foundation.e.apps.feature.auth.login
import android.accounts.AccountManager
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import foundation.e.apps.R
@@ -36,6 +38,11 @@ class SystemWideGoogleLoginLauncher(
    private val onCredentialsReady: (String, String) -> Unit,
    private val onError: (String) -> Unit,
) {
    companion object {
        private const val KEY_PENDING_ACCOUNT_NAME = "system_wide_google_login_pending_account_name"
        private const val KEY_PENDING_CONSENT_INTENT = "system_wide_google_login_pending_consent_intent"
    }

    private val coordinator = SystemWideGoogleLoginCoordinator()
    private var pendingConsentIntent: Intent? = null

@@ -68,6 +75,24 @@ class SystemWideGoogleLoginLauncher(
        )
    }

    fun restore(savedInstanceState: Bundle?) {
        if (savedInstanceState == null) return

        coordinator.restorePendingAccountName(
            savedInstanceState.getString(KEY_PENDING_ACCOUNT_NAME),
        )
        pendingConsentIntent = BundleCompat.getParcelable(
            savedInstanceState,
            KEY_PENDING_CONSENT_INTENT,
            Intent::class.java,
        )
    }

    fun save(outState: Bundle) {
        outState.putString(KEY_PENDING_ACCOUNT_NAME, coordinator.snapshotPendingAccountName())
        outState.putParcelable(KEY_PENDING_CONSENT_INTENT, pendingConsentIntent)
    }

    private fun fetchSelectedAccount(accountName: String) {
        fragment.lifecycleScope.launch {
            val fetchResult = microgAccountFetcher.fetchAccount(accountName)
Loading