From 47da262c5b47beb54835eb0a68a8ae00eaf37050 Mon Sep 17 00:00:00 2001 From: Damir Mihaljinec Date: Thu, 4 Dec 2025 00:36:30 +0100 Subject: [PATCH] feat(network): Added handler for feature disabled proton error code. --- .../dagger/CoreFeatureFlagModule.kt | 33 +++++++- .../listener/FeatureDisabledListenerImpl.kt | 60 +++++++++++++++ .../data/src/main/res/values/config.xml | 1 + .../data/src/main/res/values/public.xml | 1 + .../listener/FeatureDisabledListenerTest.kt | 77 +++++++++++++++++++ .../core/network/dagger/CoreNetworkModule.kt | 3 + .../core/network/data/ApiManagerFactory.kt | 6 ++ .../core/network/data/ApiManagerTests.kt | 3 + .../network/data/HumanVerificationTests.kt | 3 + .../proton/core/network/data/PinningTests.kt | 4 + .../network/data/ProtonApiBackendTests.kt | 3 + .../core/network/domain/ResponseCodes.kt | 1 + .../domain/feature/FeatureDisabledListener.kt | 26 +++++++ .../domain/handlers/FeatureDisabledHandler.kt | 47 +++++++++++ 14 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 feature-flag/data/src/main/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerImpl.kt create mode 100644 feature-flag/data/src/test/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerTest.kt create mode 100644 network/domain/src/main/kotlin/me/proton/core/network/domain/feature/FeatureDisabledListener.kt create mode 100644 network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/FeatureDisabledHandler.kt diff --git a/feature-flag/dagger/src/main/kotlin/me/proton/core/featureflag/dagger/CoreFeatureFlagModule.kt b/feature-flag/dagger/src/main/kotlin/me/proton/core/featureflag/dagger/CoreFeatureFlagModule.kt index 10cb582bc..7e060c351 100644 --- a/feature-flag/dagger/src/main/kotlin/me/proton/core/featureflag/dagger/CoreFeatureFlagModule.kt +++ b/feature-flag/dagger/src/main/kotlin/me/proton/core/featureflag/dagger/CoreFeatureFlagModule.kt @@ -18,12 +18,18 @@ package me.proton.core.featureflag.dagger +import android.content.Context +import android.os.SystemClock import dagger.Binds import dagger.BindsOptionalOf import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import me.proton.core.featureflag.data.FeatureFlagManagerImpl +import me.proton.core.featureflag.data.R +import me.proton.core.featureflag.data.listener.FeatureDisabledListenerImpl import me.proton.core.featureflag.data.local.FeatureFlagLocalDataSourceImpl import me.proton.core.featureflag.data.remote.FeatureFlagRemoteDataSourceImpl import me.proton.core.featureflag.data.remote.worker.FeatureFlagWorkerManagerImpl @@ -34,11 +40,36 @@ import me.proton.core.featureflag.domain.repository.FeatureFlagContextProvider import me.proton.core.featureflag.domain.repository.FeatureFlagLocalDataSource import me.proton.core.featureflag.domain.repository.FeatureFlagRemoteDataSource import me.proton.core.featureflag.domain.repository.FeatureFlagRepository +import me.proton.core.network.domain.feature.FeatureDisabledListener +import me.proton.core.network.domain.session.SessionProvider +import javax.inject.Provider import javax.inject.Singleton +import kotlin.time.DurationUnit +import kotlin.time.toDuration @Module @InstallIn(SingletonComponent::class) -public interface CoreFeatureFlagModule { +public class CoreFeatureFlagModule { + + @Provides + @Singleton + public fun provideFeatureDisabledListener( + @ApplicationContext appContext: Context, + featureFlagRepositoryProvider: Provider, + sessionProvider: SessionProvider, + ): FeatureDisabledListener = FeatureDisabledListenerImpl( + featureFlagRepositoryProvider = featureFlagRepositoryProvider, + sessionProvider = sessionProvider, + minimumFetchInterval = appContext.resources.getInteger( + R.integer.core_feature_feature_flag_on_feature_disabled_minimum_fetch_interval_seconds + ).toDuration(DurationUnit.SECONDS), + monoClock = { SystemClock.elapsedRealtime() } + ) +} + +@Module +@InstallIn(SingletonComponent::class) +public interface CoreFeatureFlagBindsModule { @Binds @Singleton diff --git a/feature-flag/data/src/main/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerImpl.kt b/feature-flag/data/src/main/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerImpl.kt new file mode 100644 index 000000000..c47b50427 --- /dev/null +++ b/feature-flag/data/src/main/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.featureflag.data.listener + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import me.proton.core.featureflag.domain.repository.FeatureFlagRepository +import me.proton.core.network.domain.feature.FeatureDisabledListener +import me.proton.core.network.domain.session.SessionId +import me.proton.core.network.domain.session.SessionProvider +import javax.inject.Inject +import javax.inject.Provider +import kotlin.time.Duration + +public class FeatureDisabledListenerImpl @Inject constructor( + private val featureFlagRepositoryProvider: Provider, + private val sessionProvider: SessionProvider, + private val minimumFetchInterval: Duration, + private val monoClock: () -> Long, +) : FeatureDisabledListener { + private var lastFetchTimestampMs: Long? = null + private val mutex = Mutex() + + override suspend fun onFeatureDisabled(sessionId: SessionId?) { + if (shouldFetch()) { + featureFlagRepositoryProvider.get()?.getAll(sessionId?.let { sessionProvider.getUserId(sessionId) }) + updateLastFetchTimestamp() + } + } + + private suspend fun shouldFetch(): Boolean = getLastFetchTimestampMs()?.let { lastFetch -> + monoClock() - lastFetch > minimumFetchInterval.inWholeMilliseconds + } ?: true + + private suspend fun getLastFetchTimestampMs(): Long? = mutex.withLock { + lastFetchTimestampMs + } + + private suspend fun updateLastFetchTimestamp() { + mutex.withLock { + lastFetchTimestampMs = monoClock() + } + } +} diff --git a/feature-flag/data/src/main/res/values/config.xml b/feature-flag/data/src/main/res/values/config.xml index 31389557d..91c60418d 100644 --- a/feature-flag/data/src/main/res/values/config.xml +++ b/feature-flag/data/src/main/res/values/config.xml @@ -18,4 +18,5 @@ 86400 21600 + 600 diff --git a/feature-flag/data/src/main/res/values/public.xml b/feature-flag/data/src/main/res/values/public.xml index 0894af05b..7f955e212 100644 --- a/feature-flag/data/src/main/res/values/public.xml +++ b/feature-flag/data/src/main/res/values/public.xml @@ -2,4 +2,5 @@ + diff --git a/feature-flag/data/src/test/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerTest.kt b/feature-flag/data/src/test/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerTest.kt new file mode 100644 index 000000000..d74f12b44 --- /dev/null +++ b/feature-flag/data/src/test/kotlin/me/proton/core/featureflag/data/listener/FeatureDisabledListenerTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.featureflag.data.listener + +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import me.proton.core.featureflag.domain.repository.FeatureFlagRepository +import me.proton.core.network.domain.feature.FeatureDisabledListener +import me.proton.core.network.domain.session.SessionProvider +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import javax.inject.Provider +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@RunWith(Parameterized::class) +class FeatureDisabledListenerTest( + private val minimumFetchIntervalSeconds: Long, + private val delayBetweenOnFeatureDisabledSeconds: Long, + private val expectedRepositoryCalls: Int, +) { + private lateinit var featureDisabledListener: FeatureDisabledListener + private val sessionProvider = mockk(relaxed = true) + private val featureFlagRepository = mockk(relaxed = true) + private val featureFlagRepositoryProvider = Provider { featureFlagRepository } + + @Test + fun testTwoCallsOnFeatureDisabled() = runTest { + //Given + featureDisabledListener = FeatureDisabledListenerImpl( + featureFlagRepositoryProvider = featureFlagRepositoryProvider, + sessionProvider = sessionProvider, + minimumFetchInterval = minimumFetchIntervalSeconds.toDuration(DurationUnit.SECONDS), + monoClock = { testScheduler.currentTime }, + ) + + //When + featureDisabledListener.onFeatureDisabled(null) + advanceTimeBy(delayBetweenOnFeatureDisabledSeconds.toDuration(DurationUnit.SECONDS)) + featureDisabledListener.onFeatureDisabled(null) + + //Then + coVerify(exactly = expectedRepositoryCalls) { + featureFlagRepository.getAll(any()) + } + } + + companion object { + @get:Parameterized.Parameters( + "minimumFetchIntervalSeconds: {0}, delayBetweenOnFeatureDisabledSeconds: {1}, expectedRepositoryCalls: {2}" + ) + @get:JvmStatic + val data = listOf( + arrayOf(3, 2, 1), + arrayOf(3, 4, 2), + ) + } +} diff --git a/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt b/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt index 481641c00..2bd8e5b95 100644 --- a/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt +++ b/network/dagger/src/main/kotlin/me/proton/core/network/dagger/CoreNetworkModule.kt @@ -52,6 +52,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider import me.proton.core.network.domain.interceptor.InterceptorInfo @@ -100,6 +101,7 @@ public class CoreNetworkModule { deviceVerificationProvider: DeviceVerificationProvider, deviceVerificationListener: DeviceVerificationListener, missingScopeListener: MissingScopeListener, + featureDisabledListener: FeatureDisabledListener, extraHeaderProvider: ExtraHeaderProvider, clientVersionValidator: ClientVersionValidator, dohAlternativesListener: DohAlternativesListener?, @@ -124,6 +126,7 @@ public class CoreNetworkModule { deviceVerificationProvider, deviceVerificationListener, missingScopeListener, + featureDisabledListener, cookieStore, CoroutineScope(Job() + Dispatchers.Default), certificatePins, diff --git a/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt b/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt index bc7bc599e..968b6298f 100644 --- a/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt +++ b/network/data/src/main/kotlin/me/proton/core/network/data/ApiManagerFactory.kt @@ -40,8 +40,10 @@ import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.handlers.DeviceVerificationNeededHandler import me.proton.core.network.domain.handlers.DohApiHandler +import me.proton.core.network.domain.handlers.FeatureDisabledHandler import me.proton.core.network.domain.handlers.HumanVerificationInvalidHandler import me.proton.core.network.domain.handlers.HumanVerificationNeededHandler import me.proton.core.network.domain.handlers.MissingScopeHandler @@ -87,6 +89,7 @@ class ApiManagerFactory( private val deviceVerificationProvider: DeviceVerificationProvider, private val deviceVerificationListener: DeviceVerificationListener, private val missingScopeListener: MissingScopeListener, + private val featureDisabledListener: FeatureDisabledListener, private val cookieStore: ProtonCookieStore, scope: CoroutineScope, private val certificatePins: Array = Constants.DEFAULT_SPKI_PINS, @@ -157,6 +160,8 @@ class ApiManagerFactory( HumanVerificationInvalidHandler(sessionId, clientIdProvider, humanVerificationListener) val deviceVerificationErrorHandler = DeviceVerificationNeededHandler(sessionId, sessionProvider, deviceVerificationListener) + val featureDisabledErrorHandler = + FeatureDisabledHandler(sessionId, featureDisabledListener) return listOf( dohApiHandler, missingScopeHandler, @@ -165,6 +170,7 @@ class ApiManagerFactory( humanVerificationInvalidHandler, humanVerificationNeededHandler, deviceVerificationErrorHandler, + featureDisabledErrorHandler, ) } diff --git a/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt b/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt index d0e2f08a0..538043da9 100644 --- a/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/ApiManagerTests.kt @@ -49,6 +49,7 @@ import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.handlers.DohApiHandler import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -96,6 +97,7 @@ internal class ApiManagerTests { private val deviceVerificationProvider = mockk() private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) + private val featureDisabledListener = mockk() private lateinit var apiManagerFactory: ApiManagerFactory private lateinit var apiManager: ApiManager @@ -163,6 +165,7 @@ internal class ApiManagerTests { deviceVerificationProvider, deviceVerificationListener, missingScopeListener, + featureDisabledListener, mockk(), testScope, cache = { null }, diff --git a/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt b/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt index e934c6ae1..f8df2eb3a 100644 --- a/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/HumanVerificationTests.kt @@ -47,6 +47,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.CookieSessionId import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -132,6 +133,7 @@ internal class HumanVerificationTests { private val deviceVerificationProvider = mockk() private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) + private val featureDisabledListener = mockk() private val clientVersionValidator = mockk { every { validate(any()) } returns true } @@ -178,6 +180,7 @@ internal class HumanVerificationTests { deviceVerificationProvider, deviceVerificationListener, missingScopeListener, + featureDisabledListener, cookieJar, scope, cache = { null }, diff --git a/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt b/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt index e78a2c00f..efa675d51 100644 --- a/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/PinningTests.kt @@ -58,6 +58,7 @@ import me.proton.core.network.domain.TimeoutOverride import me.proton.core.network.domain.client.ExtraHeaderProvider import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider import me.proton.core.network.domain.scopes.MissingScopeListener @@ -117,6 +118,9 @@ internal class PinningTests { @BindValue internal val humanVerificationProvider: HumanVerificationProvider = mockk(relaxed = true) + @BindValue + internal val featureDisabledListener: FeatureDisabledListener = mockk() + @BindValue internal val sessionListener: SessionListener = mockk() diff --git a/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt b/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt index 68ba383b4..b1f9f0235 100644 --- a/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt +++ b/network/data/src/test/java/me/proton/core/network/data/ProtonApiBackendTests.kt @@ -47,6 +47,7 @@ import me.proton.core.network.domain.client.ClientVersionValidator import me.proton.core.network.domain.client.ExtraHeaderProvider import me.proton.core.network.domain.deviceverification.DeviceVerificationListener import me.proton.core.network.domain.deviceverification.DeviceVerificationProvider +import me.proton.core.network.domain.feature.FeatureDisabledListener import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -97,6 +98,7 @@ internal class ProtonApiBackendTests { private val deviceVerificationProvider = mockk() private val deviceVerificationListener = mockk() private val missingScopeListener = mockk(relaxed = true) + private val featureDisabledListener = mockk() private val clientVersionValidator = mockk { every { validate(any()) } returns true } @@ -142,6 +144,7 @@ internal class ProtonApiBackendTests { deviceVerificationProvider, deviceVerificationListener, missingScopeListener, + featureDisabledListener, cookieJar, scope, cache = { null }, diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt index 714b16eb4..84a216180 100644 --- a/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/ResponseCodes.kt @@ -30,6 +30,7 @@ object ResponseCodes { const val NOT_SAME_AS_FIELD = 2010 const val NOT_ALLOWED = 2011 const val BANNED = 2028 + const val FEATURE_DISABLED = 2032 const val CURRENCY_FORMAT = 2053 const val NOT_EXISTS = 2501 const val APP_VERSION_BAD = 5003 diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/feature/FeatureDisabledListener.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/feature/FeatureDisabledListener.kt new file mode 100644 index 000000000..84129da8a --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/feature/FeatureDisabledListener.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.feature + +import me.proton.core.network.domain.session.SessionId + +interface FeatureDisabledListener { + + suspend fun onFeatureDisabled(sessionId: SessionId?) +} diff --git a/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/FeatureDisabledHandler.kt b/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/FeatureDisabledHandler.kt new file mode 100644 index 000000000..e748a3311 --- /dev/null +++ b/network/domain/src/main/kotlin/me/proton/core/network/domain/handlers/FeatureDisabledHandler.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Proton Technologies AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.network.domain.handlers + +import me.proton.core.network.domain.ApiBackend +import me.proton.core.network.domain.ApiErrorHandler +import me.proton.core.network.domain.ApiManager +import me.proton.core.network.domain.ApiResult +import me.proton.core.network.domain.ResponseCodes +import me.proton.core.network.domain.feature.FeatureDisabledListener +import me.proton.core.network.domain.session.SessionId +import me.proton.core.network.domain.session.SessionProvider + +class FeatureDisabledHandler( + private val sessionId: SessionId?, + private val featureDisabledListener: FeatureDisabledListener, +) : ApiErrorHandler { + + override suspend fun invoke( + backend: ApiBackend, + error: ApiResult.Error, + call: ApiManager.Call + ): ApiResult { + if (error is ApiResult.Error.Http && error.proton?.code == ResponseCodes.FEATURE_DISABLED) { + featureDisabledListener.onFeatureDisabled( + sessionId = sessionId + ) + } + return error + } +}