diff --git a/.gitignore b/.gitignore index dcdbe5fc57..25c98d5d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,3 @@ local.properties **/fastlane/Preview.html **/fastlane/screenshots **/fastlane/test_output -/buildSrc/build/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1fff3a2a88..3e26bcc1f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,117 +1,20 @@ plugins { - id("com.android.application") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") - id("com.google.gms.google-services") - id("com.google.firebase.crashlytics") - id("com.google.dagger.hilt.android") -} - -android { - compileSdk = AppConfig.compileSdkVersion - - defaultConfig { - applicationId = AppConfig.packageName - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - - versionCode = if (project.hasProperty("versionCode")) { - (project.property("versionCode") as String).toInt() - } else { - AppConfig.versionCode - } - - versionName = if (project.hasProperty("versionName")) { - project.property("versionName") as String - } else { - AppConfig.versionName - } - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildFeatures { - compose = true - viewBinding = true - } - - buildTypes { - release { - isDebuggable = false - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("prod"), - Field.TestActionEnabled(false), - Field.LogEnabled(false), - ), - builder = ::buildConfigField, - ).create() - } - - debug { - isDebuggable = true - isMinifyEnabled = false - applicationIdSuffix = ".dev" - - configure { - // disable mapping file uploads (default=true if minifying) - mappingFileUploadEnabled = false - } - - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("dev"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - - create("debug_beta") { - initWith(getByName("release")) - versionNameSuffix = "-beta" - applicationIdSuffix = ".debug" - signingConfig = signingConfigs.getByName("debug") - } - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - composeOptions { - kotlinCompilerExtensionVersion = Versions.compose - } - - packagingOptions { - resources.excludes += "lib/x86_64/darwin/libscrypt.dylib" - resources.excludes += "lib/x86_64/freebsd/libscrypt.so" - resources.excludes += "lib/x86_64/linux/libscrypt.so" - resources.excludes += "META-INF/gradle/incremental.annotation.processors" - } -} - -repositories { - mavenCentral() - maven(url = "https://jitpack.io") + alias(deps.plugins.android.application) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.google.services) + alias(deps.plugins.hilt.android) + alias(deps.plugins.firebase.crashlytics) + id("configuration") } dependencies { - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) + implementation(files("libs/walletconnect-1.5.6.aar")) implementation(project(":domain")) implementation(project(":common")) implementation(project(":core:analytics")) + implementation(project(":core:featuretoggles")) implementation(project(":core:res")) implementation(project(":core:ui")) implementation(project(":core:datasource")) @@ -123,91 +26,93 @@ dependencies { implementation(project(":features:referral:presentation")) implementation(project(":features:referral:domain")) implementation(project(":features:referral:data")) + implementation(project(":features:swap:api")) implementation(project(":features:swap:presentation")) implementation(project(":features:swap:domain")) implementation(project(":features:swap:data")) + implementation(project(":features:tester:api")) + implementation(project(":features:tester:impl")) /** AndroidX libraries */ - implementation(AndroidX.coreKtx) - implementation(AndroidX.appCompat) - implementation(AndroidX.fragmentKtx) - implementation(AndroidX.constraintLayout) - implementation(AndroidX.browser) - implementation(AndroidX.lifecycleRuntimeKtx) - implementation(AndroidX.lifecycleCommonJava8) - implementation(AndroidX.lifecycleViewModelKtx) - implementation(AndroidX.lifecycleLiveDataKtx) - implementation(AndroidX.activityCompose) + implementation(deps.androidx.core.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.constraintLayout) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.browser) + implementation(deps.lifecycle.runtime.ktx) + implementation(deps.lifecycle.common.java8) + implementation(deps.lifecycle.viewModel.ktx) /** Compose libraries */ - implementation(Compose.material) - implementation(Compose.animation) - implementation(Compose.foundation) - implementation(Compose.ui) - implementation(Compose.uiTooling) - implementation(Compose.coil) + implementation(deps.compose.material) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.compose.shimmer) /** Firebase libraries */ - implementation(platform(Firebase.bom)) - implementation(Firebase.firebaseAnalytics) - implementation(Firebase.firebaseCrashlytics) + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.firebase.crashlytics) /** Tangem libraries */ - implementation(Tangem.blockchain) { + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } - implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) { + implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { exclude(module = "joda-time") } /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) /** Other libraries */ - implementation(Library.materialComponent) - implementation(Library.googlePlayCore) - implementation(Library.googlePlayCoreKtx) - coreLibraryDesugaring(Library.desugarJdkLibs) - implementation(Library.timber) - implementation(Library.reKotlin) - implementation(Library.zxingQrCore) - implementation(Library.zxingQrBarcodeScanner) - implementation(Library.otaliastudiosCameraView) - implementation(Library.coil) - implementation(Library.appsflyer) - implementation(Library.amplitude) - implementation(Library.kotsonGsonExt) + implementation(deps.material) + implementation(deps.googlePlay.core) + implementation(deps.googlePlay.core.ktx) + implementation(deps.googlePlay.services.wallet) + coreLibraryDesugaring(deps.desugar) + implementation(deps.timber) + implementation(deps.reKotlin) + implementation(deps.zxing.qrCore) + implementation(deps.zxing.qrBarcodeScanner) + implementation(deps.otaliastudiosCameraView) + implementation(deps.coil) + implementation(deps.appsflyer) + implementation(deps.amplitude) + implementation(deps.kotsonGson) //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) - implementation(Library.moshi) - implementation(Library.moshiKotlin) - implementation(Library.okHttp) - implementation(Library.okHttpLogging) - implementation(Library.zendeskChat) - implementation(Library.zendeskMessaging) - implementation(Library.spongecastleCryptoCore) - implementation(Library.lottie) - implementation(Library.shopifyBuySdk) { + implementation(deps.retrofit) + implementation(deps.retrofit.moshi) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.okHttp) + implementation(deps.okHttp.logging) + implementation(deps.zendesk.chat) + implementation(deps.zendesk.messaging) + implementation(deps.spongecastle.core) + implementation(deps.lottie) + implementation(deps.shopify.buy) { exclude(group = "com.shopify.graphql.support") exclude(module = "joda-time") } - implementation(Library.accompanistAppCompatTheme) - implementation(Library.accompanistSystemUiController) - implementation(Library.accompanistWebView) - implementation(Library.xmlShimmer) - implementation(Library.viewBindingDelegate) - implementation(Library.armadillo) - implementation(Library.googlePlayServicesWallet) - implementation(Library.composeShimmer) - implementation(Library.mviCoreWatcher) - implementation(Library.kotlinSerialization) + implementation(deps.compose.accompanist.appCompatTheme) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.webView) + implementation(deps.xmlShimmer) + implementation(deps.viewBindingDelegate) + implementation(deps.armadillo) + implementation(deps.mviCore.watcher) + implementation(deps.kotlin.serialization) /** Testing libraries */ - testImplementation(Test.junit) - testImplementation(Test.truth) - androidTestImplementation(Test.junitAndroidExt) - androidTestImplementation(Test.espresso) + testImplementation(deps.test.junit) + testImplementation(deps.test.truth) + androidTestImplementation(deps.test.junit.android) + androidTestImplementation(deps.test.espresso) } \ No newline at end of file diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 2784e858d0..0000000000 --- a/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/debug/res/values-ru/strings.xml b/app/src/debug/res/values-ru/strings.xml index 682b552975..9c7f1adda2 100644 --- a/app/src/debug/res/values-ru/strings.xml +++ b/app/src/debug/res/values-ru/strings.xml @@ -1,6 +1,6 @@ - DebugTangem + DebugTangem - \ No newline at end of file + diff --git a/app/src/debug/res/xml/network_security_config.xml b/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000000..894912ac61 --- /dev/null +++ b/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/debug_beta/AndroidManifest.xml b/app/src/debug_beta/AndroidManifest.xml deleted file mode 100644 index 2784e858d0..0000000000 --- a/app/src/debug_beta/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/debug_beta/res/values-ru/strings.xml b/app/src/debug_beta/res/values-ru/strings.xml deleted file mode 100644 index 3ae149049f..0000000000 --- a/app/src/debug_beta/res/values-ru/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - BetaTangem - - \ No newline at end of file diff --git a/app/src/debug_beta/res/values/strings.xml b/app/src/debug_beta/res/values/strings.xml deleted file mode 100644 index 3ae149049f..0000000000 --- a/app/src/debug_beta/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - BetaTangem - - \ No newline at end of file diff --git a/app/src/external/res/values-ru/strings.xml b/app/src/external/res/values-ru/strings.xml new file mode 100644 index 0000000000..3a1e80c609 --- /dev/null +++ b/app/src/external/res/values-ru/strings.xml @@ -0,0 +1,6 @@ + + + + ExternalTangem + + diff --git a/app/src/external/res/values/strings.xml b/app/src/external/res/values/strings.xml new file mode 100644 index 0000000000..3a1e80c609 --- /dev/null +++ b/app/src/external/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + ExternalTangem + + diff --git a/app/src/internal/res/values-ru/strings.xml b/app/src/internal/res/values-ru/strings.xml new file mode 100644 index 0000000000..76df805cb3 --- /dev/null +++ b/app/src/internal/res/values-ru/strings.xml @@ -0,0 +1,6 @@ + + + + InternalTangem + + diff --git a/app/src/internal/res/values/strings.xml b/app/src/internal/res/values/strings.xml new file mode 100644 index 0000000000..76df805cb3 --- /dev/null +++ b/app/src/internal/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + InternalTangem + + diff --git a/app/src/internal/res/xml/network_security_config.xml b/app/src/internal/res/xml/network_security_config.xml new file mode 100644 index 0000000000..894912ac61 --- /dev/null +++ b/app/src/internal/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index f5f6ac9548..c714678d9c 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit f5f6ac954894ef66331db8fe383b9773feb69ef4 +Subproject commit c714678d9c46dee1b15ef9e8d27aa23afa1dfd8d diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 14d367046b..7ad61d6a45 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -10,9 +10,10 @@ import androidx.core.view.WindowInsetsControllerCompat import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar import com.tangem.TangemSdk +import com.tangem.features.tester.api.TesterRouter import com.tangem.operations.backup.BackupService -import com.tangem.tangem_sdk_new.extensions.init -import com.tangem.tangem_sdk_new.extensions.initWithBiometrics +import com.tangem.sdk.extensions.init +import com.tangem.sdk.extensions.initWithBiometrics import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback @@ -33,6 +34,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding @@ -71,6 +73,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var appStateHolder: AppStateHolder + /** Router for opening tester menu */ + @Inject + lateinit var testerRouter: TesterRouter + private var snackbar: Snackbar? = null private val dialogManager = DialogManager() private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) @@ -97,6 +103,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac GooglePayService(createPaymentsClient(this), this), ), ) + + store.dispatch(DaggerGraphAction.SetActivityDependencies(testerRouter)) } private fun initUserWalletsListManager() { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 42d008074f..04f9a65ebc 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -11,12 +11,13 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config -import com.tangem.datasource.utils.AndroidAssetReader -import com.tangem.datasource.utils.AssetReader +import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig import com.tangem.tap.common.IntentHandler @@ -48,11 +49,12 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository -import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.launch import okhttp3.logging.HttpLoggingInterceptor import org.rekotlin.Store import timber.log.Timber @@ -65,7 +67,6 @@ lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesStorage lateinit var walletConnectRepository: WalletConnectRepository lateinit var shopService: TangemShopService -lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() } @@ -116,6 +117,15 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var configManager: ConfigManager + @Inject + lateinit var assetReader: AssetReader + + @Inject + lateinit var featureTogglesManager: FeatureTogglesManager + + @Inject + lateinit var networkConnectionManager: NetworkConnectionManager + override fun onCreate() { super.onCreate() @@ -136,11 +146,9 @@ class TapApplication : Application(), ImageLoaderFactory { registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) DomainLayer.init() - NetworkConnectivity.createInstance(store, this) preferencesStorage = PreferencesStorage(this) walletConnectRepository = WalletConnectRepository(this) - assetReader = AndroidAssetReader(this) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -154,10 +162,23 @@ class TapApplication : Application(), ImageLoaderFactory { userTokensRepository = UserTokensRepository.init( context = this, tangemTechService = store.state.domainNetworks.tangemTechService, + networkConnectionManager = networkConnectionManager, ) appStateHolder.mainStore = store appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager + + store.dispatch( + action = DaggerGraphAction.SetApplicationDependencies( + assetReader = assetReader, + networkConnectionManager = networkConnectionManager, + ), + ) + + scope.launch { + featureTogglesManager.init() + } + initTopUpController() } @@ -217,6 +238,7 @@ class TapApplication : Application(), ImageLoaderFactory { ) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { appVersion = try { + // TODO don't use deprecated method val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) pInfo.versionName } catch (e: PackageManager.NameNotFoundException) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index ce8e64b0f6..8bbf95d217 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -24,6 +24,8 @@ sealed class AnalyticsParam { sealed class CardBalanceState(val value: String) { object Empty : CardBalanceState("Empty") object Full : CardBalanceState("Full") + object CustomToken : CardBalanceState("Custom token") + object BlockchainError : CardBalanceState("Blockchain error") companion object } @@ -79,6 +81,7 @@ sealed class AnalyticsParam { companion object Key { const val Source = "Source" + const val Balance = "Balance" const val Batch = "Batch" const val ProductType = "Product Type" const val Firmware = "Firmware" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index 4a14854595..e8fe4ec98a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -11,6 +11,13 @@ sealed class Basic( error: Throwable? = null, ) : AnalyticsEvent("Basic", event, params, error) { + class BalanceLoaded(balance: AnalyticsParam.CardBalanceState) : Basic( + event = "Balance Loaded", + params = mapOf( + AnalyticsParam.Balance to balance.value, + ), + ) + class CardWasScanned( source: AnalyticsParam.ScannedFrom, ) : Basic( @@ -51,4 +58,6 @@ sealed class Basic( event = "Scan", error = error, ) + + class WalletOpened : Basic(event = "Wallet Opened") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt index 6c8580df05..7a36f35d1e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -11,7 +11,6 @@ sealed class MainScreen( ) : AnalyticsEvent("Main Screen", event, params) { class ScreenOpened : MainScreen("Screen opened") - class ButtonScanCard : MainScreen("Button - Scan Card") class ButtonMyWallets : MainScreen("Button - My Wallets") class ButtonBuy : MainScreen("Button - Buy") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index c0d61c52d1..d684ed3cce 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -71,16 +71,16 @@ sealed class Onboarding( class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code") class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened") class ButtonConnect : Onboarding("Onboarding", "Button - Connect") - class PinCodeSet : Onboarding("Onboarding", "PIN code set") + class PinCodeSet : Onboarding("Onboarding", "PIN Code Set") class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened") - class KYCStarted : Onboarding("Onboarding", "KYC started") - class KYCInProgress : Onboarding("Onboarding", "KYC in progress") - class KYCRejected : Onboarding("Onboarding", "KYC rejected") + class KYCStarted : Onboarding("Onboarding", "KYC Started") + class KYCInProgress : Onboarding("Onboarding", "KYC In Progress") + class KYCRejected : Onboarding("Onboarding", "KYC Rejected") - class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened") + class ClaimScreenOpened : Onboarding("Onboarding", "Claim Screen Opened") class ButtonClaim : Onboarding("Onboarding", "Button - Claim") - class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully") + class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully") class ButtonChat : Onboarding("Onboarding", "Button - Chat") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index df526f80d5..8a2fceadaf 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -60,16 +60,16 @@ sealed class Settings( params: Map = mapOf(), ) : Settings(category = "Settings / App Settings", event = event, params = params) { - class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( + class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : AppSettings( event = "Save Wallet Switcher Changed", params = mapOf("State" to state.value), ) - class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( + class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : AppSettings( event = "Save Access Code Switcher Changed", params = mapOf("State" to state.value), ) - object ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication") + object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 6247c504b3..874338b33b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -48,7 +48,7 @@ sealed class Token( class Bought(type: CurrencyType) : Token( category = "Token", - event = "Token bought", + event = "Token Bought", params = mapOf("Token" to type.value), ) diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt index fa91934a30..32c3110ba5 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.tangem.tangem_sdk_new.extensions.dpToPx -import com.tangem.tangem_sdk_new.extensions.pxToDp +import com.tangem.sdk.extensions.dpToPx +import com.tangem.sdk.extensions.pxToDp /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 133869ee22..4f9ab200f1 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -36,6 +36,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Kusama -> R.drawable.ic_kusama_no_color Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color Blockchain.Dash -> R.drawable.ic_dash_no_color + Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color else -> R.drawable.ic_tangem_logo } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index bfd3fa99c2..8b5eafb76f 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -42,8 +42,12 @@ fun Store<*>.dispatchNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowNotification(resId)) } -suspend fun Store.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { - state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh) +suspend fun Store.onUserWalletSelected( + userWallet: UserWallet, + refresh: Boolean = false, + sendAnalyticsEvent: Boolean = false, +) { + state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh, sendAnalyticsEvent) } fun Store<*>.dispatchToastNotification(resId: Int) { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 0939e05be9..0930704d50 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -16,8 +16,8 @@ import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.reducers.createAddressesData -import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import kotlinx.coroutines.delay import timber.log.Timber @@ -47,7 +47,8 @@ suspend fun WalletManager.safeUpdate(): Result = try { } catch (exception: Exception) { Timber.e(exception) - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + if (!networkConnectionManager.isOnline) { Result.Failure(TapError.NoInternetConnection) } else { val blockchain = wallet.blockchain @@ -87,8 +88,6 @@ fun WalletManager?.getAddressData(): AddressData? { return if (addressDataList.isEmpty()) null else addressDataList[0] } -fun WalletManager.getTxHistory(currency: Currency): List = wallet.getTxHistory(currency) - fun WalletManager.getBlockchainTxHistory(): List = wallet.getBlockchainTxHistory() fun WalletManager.getTokenTxHistory(token: Token): List = wallet.getTokenTxHistory(token) diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt index 98a06327bb..8253e64b37 100644 --- a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt +++ b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt @@ -6,8 +6,8 @@ import android.widget.FrameLayout import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidgetState import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.wallet.R import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding diff --git a/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt b/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt index b1d16fc3f6..cb8ea5186f 100644 --- a/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt +++ b/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt @@ -3,7 +3,7 @@ package com.tangem.tap.common.recyclerView import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.sdk.extensions.dpToPx class SpaceItemDecoration( private val horizontalSpaceDp: Float, diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index ec25507402..cb18e36047 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -20,6 +20,7 @@ import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState { @@ -46,6 +47,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) walletSelectorState = WalletSelectorReducer.reduce(action, state), sprinklrState = SprinklrReducer.reduce(action, state), signInState = SignInReducer.reduce(action, state), + daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index c4d49c0ae2..d3000eb374 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -43,6 +43,8 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState +import com.tangem.tap.proxy.redux.DaggerGraphMiddleware +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import org.rekotlin.Middleware import org.rekotlin.StateType @@ -66,7 +68,8 @@ data class AppState( val saveWalletState: SaveWalletState = SaveWalletState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(), val sprinklrState: SprinklrState = SprinklrState(), - val signInState: SignInState = SignInState() + val signInState: SignInState = SignInState(), + val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { val domainState: DomainState @@ -108,7 +111,8 @@ data class AppState( LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, SprinklrMiddleware().middleware, - SignInMiddleware.middleware + SignInMiddleware.middleware, + DaggerGraphMiddleware.daggerGraphMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 509cd78033..449192d23a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction @@ -36,8 +37,11 @@ class TapWalletManager { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } - suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) { + suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) { Analytics.setContext(userWallet.scanResponse) + if (sendAnalyticsEvent) { + Analytics.send(Basic.WalletOpened()) + } val scanResponse = userWallet.scanResponse val card = scanResponse.card val attestationFailed = card.attestation.status == Attestation.Status.Failed diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt index bc1f1dccc5..51c4c304e2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt @@ -6,8 +6,8 @@ import com.tangem.common.services.Result import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.datasource.utils.AssetReader import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -60,7 +60,7 @@ class LoadAvailableCoinsService( return withContext(dispatchers.io) { runCatching { tangemTechApi.getCoins( - networkIds = supportedBlockchains.toSet().map(Blockchain::toNetworkId).joinToString(","), + networkIds = supportedBlockchains.toSet().joinToString(",", transform = Blockchain::toNetworkId), active = true, searchText = searchInput, offset = offset, @@ -74,8 +74,8 @@ class LoadAvailableCoinsService( } } - fun getTestnetCoins(): List { - val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS) + private fun getTestnetCoins(): List { + val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) return currenciesAdapter.fromJson(json)!!.coins .map { Currency.fromJsonObject(it) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt deleted file mode 100644 index b9338e42a3..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.tap.common.FileReader -import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.domain.tokens.models.TokenDao -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext - -@Deprecated("Use this only for migration") -class OldUserTokensRepository( - private val fileReader: FileReader, - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) { - private val moshi = MoshiConverter.networkMoshi - private val blockchainsAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, Blockchain::class.java), - ) - private val tokensAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, TokenDao::class.java), - ) - private val blockchainNetworkAdapter: JsonAdapter> = - moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java)) - - @Deprecated("Use BlockchainNetwork instead") - private fun loadSavedTokens(cardId: String): List { - val json = try { - fileReader.readFile(getFileNameForTokens(cardId)) - } catch (exception: Exception) { - return emptyList() - } - - return try { - tokensAdapter.fromJson(json) ?: emptyList() - } catch (exception: Exception) { - emptyList() - } - } - - @Deprecated("Use BlockchainNetwork instead") - private fun loadSavedBlockchains(cardId: String): List { - return try { - val json = fileReader.readFile(getFileNameForBlockchains(cardId)) - blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList() - } catch (exception: Exception) { - emptyList() - } - } - - @Deprecated("Use TokensRepository instead") - suspend fun loadSavedCurrencies( - cardId: String, - isHdWalletSupported: Boolean = false, - ): List { - return try { - val json = fileReader.readFile(getFileNameForBlockchains(cardId)) - blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList() - } catch (exception: Exception) { - tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported) - } - } - - private suspend fun tryToLoadPreviousFormatAndMigrate( - cardId: String, - isHdWalletSupported: Boolean = false, - ): List { - return try { - loadSavedCurrenciesOldWay( - cardId, - isHdWalletSupported, - ) - } catch (exception: Exception) { - emptyList() - } - } - - private suspend fun loadSavedCurrenciesOldWay( - cardId: String, - isHdWalletSupported: Boolean = false, - ): List { - val blockchains = loadSavedBlockchains(cardId) - val tokens = loadSavedTokens(cardId) - val ids = getTokensIds(tokens) - val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null - val blockchainNetworks = blockchains.map { blockchain -> - BlockchainNetwork( - blockchain = blockchain, - derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath, - tokens = tokens - .filter { it.blockchainDao.toBlockchain() == blockchain } - .map { - val token = it.toToken() - token.copy(id = ids[token.contractAddress]) - }, - ) - } - return blockchainNetworks - } - - private suspend fun getTokensIds(tokens: List): Map = withContext(dispatchers.io) { - tokens.map { - async { - tangemTechApi.getCoins( - contractAddress = it.contractAddress, - networkIds = it.blockchainDao.toBlockchain().toNetworkId(), - active = true, - ) - } - } - .map { - runCatching { it.await() } - .onSuccess { return@map it.coins.firstOrNull()?.id } - .onFailure { return@map null } - - error("Unreachable code because runCatching must return result") - } - .mapIndexedNotNull { index, id -> - if (id == null) null else tokens[index].contractAddress to id - } - .toMap() - } - - companion object { - private const val FILE_NAME_PREFIX_TOKENS = "tokens" - private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains" - private fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId" - private fun getFileNameForBlockchains(cardId: String): String = - "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 03b431d3e2..175f72a121 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.common.CardDTO import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.domain.model.builders.UserWalletIdBuilder @@ -15,7 +16,6 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.toBlockchainNetworks import com.tangem.tap.features.wallet.models.toCurrencies -import com.tangem.tap.network.NetworkConnectivity import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -25,33 +25,31 @@ class UserTokensRepository( private val storageService: UserTokensStorageService, private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, + private val networkConnectionManager: NetworkConnectionManager, ) { - // TODO("After adding DI") replace with CoroutineDispatcherProvider suspend fun getUserTokens(card: CardDTO): List = withContext(dispatchers.io) { - val userId = getUserWalletId(card) ?: return@withContext emptyList() + val userWalletId = getUserWalletId(card) ?: return@withContext emptyList() + if (DemoHelper.isDemoCardId(card.cardId)) { - return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies) + return@withContext loadTokensOffline(userWalletId = userWalletId).ifEmpty(::loadDemoCurrencies) } - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { - return@withContext loadTokensOffline(card, userId) - } + if (!networkConnectionManager.isOnline) return@withContext loadTokensOffline(userWalletId = userWalletId) - return@withContext remoteGetUserTokens(card, userId) + return@withContext remoteGetUserTokens(userWalletId = userWalletId) } - // TODO("After adding DI") replace with CoroutineDispatcherProvider suspend fun saveUserTokens(card: CardDTO, tokens: List) = withContext(dispatchers.io) { - val userId = getUserWalletId(card) ?: return@withContext + val userWalletId = getUserWalletId(card) ?: return@withContext val userTokens = tokens.toUserTokensResponse() - remoteSaveUserTokens(userId, userTokens) - storageService.saveUserTokens(userId, userTokens) + remoteSaveUserTokens(userWalletId = userWalletId, userTokens = userTokens) + storageService.saveUserTokens(userWalletId = userWalletId, tokens = userTokens) } suspend fun loadBlockchainsToDerive(card: CardDTO): List = withContext(dispatchers.io) { - val userId = getUserWalletId(card) ?: return@withContext emptyList() - val blockchainNetworks = loadTokensOffline(card = card, userId = userId).toBlockchainNetworks() + val userWalletId = getUserWalletId(card) ?: return@withContext emptyList() + val blockchainNetworks = loadTokensOffline(userWalletId = userWalletId).toBlockchainNetworks() if (DemoHelper.isDemoCardId(card.cardId)) { return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks) @@ -60,8 +58,8 @@ class UserTokensRepository( return@withContext blockchainNetworks } - private suspend fun loadTokensOffline(card: CardDTO, userId: String): List { - return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) + private fun loadTokensOffline(userWalletId: String): List { + return storageService.getUserTokens(userWalletId = userWalletId) ?: emptyList() } private fun loadDemoCurrencies(): List { @@ -76,54 +74,49 @@ class UserTokensRepository( .flatMap(BlockchainNetwork::toCurrencies) } - private fun List.toUserTokensResponse() = UserTokensResponse( - tokens = CurrencyConverter.convertList(this), - group = GROUP_DEFAULT_VALUE, - sort = SORT_DEFAULT_VALUE, - ) + private fun List.toUserTokensResponse(): UserTokensResponse { + return UserTokensResponse( + tokens = CurrencyConverter.convertList(input = this), + group = GROUP_DEFAULT_VALUE, + sort = SORT_DEFAULT_VALUE, + ) + } - private suspend fun handleGetUserTokensFailure(card: CardDTO, userId: String, error: Throwable): List { + private suspend fun handleGetUserTokensFailure(userWalletId: String, error: Throwable): List { return when { - error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> - storageService.getUserTokens(card).also { - remoteSaveUserTokens(userId = userId, userTokens = it.toUserTokensResponse()) - } + error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> { + storageService + .getUserTokens(userWalletId) + ?.also { remoteSaveUserTokens(userWalletId = userWalletId, userTokens = it.toUserTokensResponse()) } + ?: emptyList() + } else -> { - val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) - tokens.distinct() + storageService.getUserTokens(userWalletId)?.distinct() ?: emptyList() } } } - private suspend fun remoteGetUserTokens(card: CardDTO, userId: String): List { - runCatching { - tangemTechApi.getUserTokens(userId) - }.onSuccess { response -> - return response.tokens - .mapNotNull(Currency.Companion::fromTokenResponse).also { - storageService.saveUserTokens(userId, it.toUserTokensResponse()) - } - .distinct() - }.onFailure { - return handleGetUserTokensFailure(card = card, userId = userId, error = it) - } - error("Unreachable code because runCatching must return result") + private suspend fun remoteGetUserTokens(userWalletId: String): List { + return runCatching { tangemTechApi.getUserTokens(userWalletId) } + .fold( + onSuccess = { response -> + response.tokens + .mapNotNull(Currency.Companion::fromTokenResponse) + .also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) } + .distinct() + }, + onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) }, + ) } - private suspend fun remoteSaveUserTokens(userId: String, userTokens: UserTokensResponse) { + private suspend fun remoteSaveUserTokens(userWalletId: String, userTokens: UserTokensResponse) { // it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR // if the /user-tokens endpoint disabled - runCatching { - tangemTechApi.saveUserTokens(userId, userTokens) - }.onFailure { - Timber.e(it) - } + runCatching { tangemTechApi.saveUserTokens(userWalletId, userTokens) } + .onFailure { Timber.e(it) } } - private fun getUserWalletId(card: CardDTO): String? { - return UserWalletIdBuilder.card(card).build() - ?.stringValue - } + private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue companion object { private const val GROUP_DEFAULT_VALUE = "none" @@ -131,24 +124,16 @@ class UserTokensRepository( private const val NOT_FOUND_HTTP_CODE = "404" // TODO("After adding DI") get dependencies by DI - fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository { - val fileReader = AndroidFileReader(context) - val dispatchers = AppCoroutineDispatcherProvider() - - val oldUserTokensRepository = OldUserTokensRepository( - fileReader = fileReader, - tangemTechApi = tangemTechService.api, - dispatchers = dispatchers, - ) - val storageService = UserTokensStorageService( - oldUserTokensRepository = oldUserTokensRepository, - fileReader = fileReader, - ) - + fun init( + context: Context, + tangemTechService: TangemTechService, + networkConnectionManager: NetworkConnectionManager, + ): UserTokensRepository { return UserTokensRepository( - storageService = storageService, + storageService = UserTokensStorageService(fileReader = AndroidFileReader(context)), tangemTechApi = tangemTechService.api, - dispatchers = dispatchers, + dispatchers = AppCoroutineDispatcherProvider(), + networkConnectionManager = networkConnectionManager, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index 9df51fb6f4..8ebee84b97 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -4,21 +4,16 @@ import com.squareup.moshi.JsonAdapter import com.tangem.Log import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.CardDTO import com.tangem.tap.common.FileReader import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.toCurrencies -class UserTokensStorageService( - private val oldUserTokensRepository: OldUserTokensRepository, - private val fileReader: FileReader, -) { +class UserTokensStorageService(private val fileReader: FileReader) { private val userTokensAdapter: JsonAdapter = MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java) - fun getUserTokens(userId: String): List? { + fun getUserTokens(userWalletId: String): List? { return try { - val json = fileReader.readFile(getFileNameForUserTokens(userId)) + val json = fileReader.readFile(getFileNameForUserTokens(userWalletId)) userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) } } catch (exception: Exception) { Log.error { exception.stackTraceToString() } @@ -26,16 +21,9 @@ class UserTokensStorageService( } } - @Deprecated("") - suspend fun getUserTokens(card: CardDTO): List { - val blockchainNetworks = - oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed) - return blockchainNetworks.flatMap { it.toCurrencies() } - } - - fun saveUserTokens(userId: String, tokens: UserTokensResponse) { + fun saveUserTokens(userWalletId: String, tokens: UserTokensResponse) { val json = userTokensAdapter.toJson(tokens) - fileReader.rewriteFile(json, getFileNameForUserTokens(userId)) + fileReader.rewriteFile(json, getFileNameForUserTokens(userWalletId)) } companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 8e2cea35fa..3795a0cf00 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -9,10 +9,10 @@ import com.tangem.common.KeyPair import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.operations.wallet.CreateWalletResponse -import com.tangem.datasource.utils.AssetReader import com.tangem.tap.tangemSdkManager class TwinCardsManager( @@ -89,7 +89,7 @@ class TwinCardsManager( } private fun getIssuers(reader: AssetReader): List { - val file = reader.readAssetAsString("tangem-app-config/issuers") + val file = reader.readJson(fileName = "tangem-app-config/issuers") return getAdapter().fromJson(file)!! } } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt index 95d1d76fdd..ca1f95a650 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt @@ -7,9 +7,9 @@ import android.view.View import androidx.core.animation.doOnEnd import com.tangem.common.extensions.VoidCallback import com.tangem.domain.common.TwinCardNumber -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 108547e8b2..76f11e8ae0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -5,8 +5,8 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage -import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage -import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences +import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index f42dd9ced5..d2936d9dcc 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -44,7 +44,8 @@ import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransactionType import com.tangem.tap.features.wallet.models.filterByCoin import com.tangem.tap.features.wallet.models.getPendingTransactions -import com.tangem.tap.network.NetworkConnectivity +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -62,8 +63,6 @@ internal class DefaultWalletAmountsRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, ) : WalletAmountsRepository { - private val walletStoresStorage = WalletStoresStorage - private val walletManagersStorage = WalletManagerStorage override suspend fun updateAmountsForUserWallets( userWallets: List, @@ -76,8 +75,7 @@ internal class DefaultWalletAmountsRepository( awaitAll( async { fetchAmountsForUserWallets(userWallets) }, async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) }, - ) - .fold() + ).fold() } } } @@ -104,8 +102,7 @@ internal class DefaultWalletAmountsRepository( awaitAll( async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) }, async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) }, - ) - .fold() + ).fold() } } } @@ -123,41 +120,41 @@ internal class DefaultWalletAmountsRepository( walletStores: List?, fiatCurrency: FiatCurrency, ): CompletionResult { - // FIXME: Use NetworkConnectionManager when it is added to DI - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + if (!networkConnectionManager.isOnline) { return CompletionResult.Failure(WalletStoresError.NoInternetConnection) } val walletStoresInternal = walletStores ?: getWalletStores(userWallets) - val currencies = walletStoresInternal - .asSequence() - .flatMap { it.walletsData } - .map { it.currency } + val currencies = walletStoresInternal.asSequence().flatMap { it.walletsData }.map { it.currency } val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList() return withContext(dispatchers.io) { - runCatching { tangemTechApi.getRates(fiatCurrency.code.lowercase(), coinsIds.joinToString(",")) } - .onSuccess { - updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates) - return@withContext CompletionResult.Success(Unit) - } - .onFailure { - val error = WalletStoresError.FetchFiatRatesError( - currencies = currencies.map(Currency::currencySymbol).toList(), - cause = it, - ) + runCatching { + tangemTechApi.getRates( + fiatCurrency.code.lowercase(), + coinsIds.joinToString(","), + ) + }.onSuccess { + updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates) + return@withContext CompletionResult.Success(Unit) + }.onFailure { + val error = WalletStoresError.FetchFiatRatesError( + currencies = currencies.map(Currency::currencySymbol).toList(), + cause = it, + ) - Timber.e( - error, - """ + Timber.e( + error, + """ Unable to fetch fiat rates |- Coins ids: $coinsIds - """.trimIndent(), - ) + """.trimIndent(), + ) - return@withContext CompletionResult.Failure(error) - } + return@withContext CompletionResult.Failure(error) + } error("Unreachable code because runCatching must return result") } @@ -166,9 +163,7 @@ internal class DefaultWalletAmountsRepository( private suspend fun fetchAmountsForUserWallets( userWallets: List, ): CompletionResult = withContext(Dispatchers.Default) { - userWallets.map { async { fetchAmountsForUserWallet(it) } } - .awaitAll() - .fold() + userWallets.map { async { fetchAmountsForUserWallet(it) } }.awaitAll().fold() } private suspend fun fetchAmountsForUserWallet( @@ -186,8 +181,8 @@ internal class DefaultWalletAmountsRepository( scanResponse: ScanResponse, walletStores: List, ): CompletionResult = coroutineScope { - // FIXME: Use NetworkConnectionManager when it is added to DI - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + if (!networkConnectionManager.isOnline) { walletStores.forEach { updateWalletStoreWithUnreachable(it) } @@ -200,9 +195,7 @@ internal class DefaultWalletAmountsRepository( val walletManager = walletStore.walletManager fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager) } - } - .awaitAll() - .fold() + }.awaitAll().fold() } private suspend fun fetchAmountsForWalletStore( @@ -223,24 +216,25 @@ internal class DefaultWalletAmountsRepository( updateWalletStoreWithUnreachable(walletStore) } else -> { - updateWalletManager(scanResponse, walletManager) - .map { updateWalletManagerInStorage(userWalletId, walletManager) } - .flatMap { - updateWalletStoreWithAmounts( - walletStore = walletStore, - updatedWallet = walletManager.wallet, - // FIXME: move DemoHelper to Demo core module maybe - isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId), - ) - } - .flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) } - .flatMapOnFailure { error -> - updateWalletStoreWithError( - walletStore = walletStore, - wallet = walletManager.wallet, - error = error, - ) - } + updateWalletManager(scanResponse, walletManager).map { + updateWalletManagerInStorage( + userWalletId, + walletManager, + ) + }.flatMap { + updateWalletStoreWithAmounts( + walletStore = walletStore, + updatedWallet = walletManager.wallet, + // FIXME: move DemoHelper to Demo core module maybe + isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId), + ) + }.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error -> + updateWalletStoreWithError( + walletStore = walletStore, + wallet = walletManager.wallet, + error = error, + ) + } } } } @@ -267,8 +261,7 @@ internal class DefaultWalletAmountsRepository( walletStore: WalletStoreModel, walletManager: WalletManager, ): CompletionResult { - val rentProvider = walletManager as? RentProvider - ?: return CompletionResult.Success(Unit) + val rentProvider = walletManager as? RentProvider ?: return CompletionResult.Success(Unit) when (val result = rentProvider.minimalBalanceForRentExemption()) { is Success -> { @@ -318,7 +311,7 @@ internal class DefaultWalletAmountsRepository( ) if (error is BlockchainSdkError) { - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStore( walletStoreToUpdate = walletStore, update = { @@ -350,7 +343,7 @@ internal class DefaultWalletAmountsRepository( """.trimIndent(), ) - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStore( walletStoreToUpdate = walletStore, update = { @@ -378,7 +371,7 @@ internal class DefaultWalletAmountsRepository( """.trimIndent(), ) - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStore( walletStoreToUpdate = walletStore, update = { @@ -402,7 +395,7 @@ internal class DefaultWalletAmountsRepository( """.trimIndent(), ) - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStore( walletStoreToUpdate = walletStore, update = { @@ -425,7 +418,7 @@ internal class DefaultWalletAmountsRepository( """.trimIndent(), ) - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStores( walletStoresToUpdate = walletStores, update = { @@ -450,7 +443,7 @@ internal class DefaultWalletAmountsRepository( ) if (rent != walletStore.walletRent) { - walletStoresStorage.update { prevState -> + WalletStoresStorage.update { prevState -> prevState.replaceWalletStore( walletStoreToUpdate = walletStore, update = { @@ -465,14 +458,12 @@ internal class DefaultWalletAmountsRepository( userWalletId: UserWalletId, walletManager: WalletManager, ) = withContext(Dispatchers.Default) { - walletManagersStorage.update { prevManagers -> - val newManagersForUserWallet = prevManagers[userWalletId].orEmpty() - .toMutableList() - .apply { - replaceByOrAdd(walletManager) { - it.wallet.blockchain == walletManager.wallet.blockchain - } + WalletManagerStorage.update { prevManagers -> + val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply { + replaceByOrAdd(walletManager) { + it.wallet.blockchain == walletManager.wallet.blockchain } + } prevManagers.apply { set(userWalletId, newManagersForUserWallet) @@ -481,13 +472,8 @@ internal class DefaultWalletAmountsRepository( } private suspend fun getWalletStores(userWallets: List): List { - return userWallets - .map { it.walletId } - .flatMap { userWalletId -> - walletStoresStorage.getAll() - .firstOrNull() - ?.get(userWalletId) - .orEmpty() - } + return userWallets.map { it.walletId }.flatMap { userWalletId -> + WalletStoresStorage.getAll().firstOrNull()?.get(userWalletId).orEmpty() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 8ac99d6590..43e31f423c 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -1,7 +1,6 @@ package com.tangem.tap.features.demo import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.TransactionSigner @@ -11,6 +10,7 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.demo.DemoConfig import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -18,10 +18,8 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store -import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import org.rekotlin.Action -import java.math.BigDecimal /** [REDACTED_AUTHOR] @@ -84,412 +82,6 @@ object DemoHelper { } } -@Suppress("LargeClass") -class DemoConfig { - - val demoBlockchains = listOf( - Blockchain.Bitcoin, - Blockchain.Ethereum, - Blockchain.Dogecoin, - Blockchain.Solana, - ) - - val demoCardIds: List by lazy { - val demoIds = getReleaseIds().toMutableList() - if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds) - - return@lazy demoIds.distinct() - } - - private val walletBalances: Map = mapOf( - Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin), - Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum), - Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin), - Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana), - ) - - fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId) - - fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId) - - fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy() - ?: Amount(BigDecimal.ZERO, blockchain).copy() - - private fun getReleaseIds(): List { - return (releaseDemoCardIds + testDemoCardIds).distinct() - } - - @Suppress("ClassOrdering") - private val releaseDemoCardIds = mutableListOf( - // === Not from the Google Sheet table === - "AC01000000041225", - "AC01000000041472", - "AB01000000046498", - "AB01000000049608", - "AB01000000049574", - "AB01000000046704", - "AB02000000051000", - "AB02000000050911", - - // === Mvideo === - // Wallet - "AC01000000045754", - "AC01000000041662", - "AC01000000041647", - "AC01000000041209", - "AC01000000042462", - "AC01000000041100", - "AC01000000041621", - "AC01000000045960", - "AC01000000041092", - "AC01000000041217", - "AC01000000013489", - "AC01000000028610", - "AC01000000028701", - "AC01000000028578", - "AC01000000027281", - "AC01000000027216", - "AC01000000028594", - "AC01000000028602", - "AC01000000028636", - "AC01000000013968", - "AC01000000027208", - "AC01000000013471", - "AC01000000028586", - "AC01000000013703", - "AC01000000028628", - "AC01000000028693", - "AC01000000028685", - "AC01000000013950", - "AC01000000013828", - "AC01000000013497", - "AC01000000013836", - "AC01000000013505", - "AC03000000046693", - "AC03000000046685", - "AC03000000046677", - "AC03000000046669", - "AC03000000046651", - "AC03000000046644", - "AC03000000046636", - "AC03000000046628", - "AC03000000046610", - "AC03000000046602", - "AC03000000046594", - "AC03000000046586", - "AC03000000046578", - "AC03000000046560", - "AC03000000046552", - "AC03000000046545", - "AC03000000046537", - "AC03000000046529", - "AC03000000046511", - "AC03000000046800", - "AC03000000046792", - "AC03000000046784", - "AC03000000046776", - "AC03000000046768", - "AC03000000046750", - "AC03000000046743", - "AC03000000046735", - "AC03000000046727", - "AC03000000046446", - "AC03000000046438", - "AC03000000046412", - "AC03000000046388", - "AC03000000046370", - "AC03000000046354", - "AC03000000046347", - "AC03000000046339", - "AC03000000046321", - "AC03000000046172", - "AC03000000046396", - "AC03000000046404", - "AC03000000046701", - "AC03000000046420", - "AC03000000046719", - "AC03000000046503", - "AC03000000046495", - "AC03000000046487", - "AC03000000046362", - "AC03000000046479", - "AC03000000046461", - "AC03000000046453", - - // Note BTC - "AB01000000059608", - "AB01000000046647", - "AB01000000046571", - "AB01000000046746", - "AB01000000059574", - "AB01000000046753", - "AB01000000046605", - "AB01000000046761", - "AB01000000046720", - "AB01000000046530", - "AB01000000016475", - "AB01000000016483", - "AB01000000016491", - "AB01000000020709", - "AB01000000020717", - "AB01000000015550", - "AB01000000015394", - "AB01000000016079", - "AB01000000016087", - "AB01000000016095", - "AB01000000020915", - "AB01000000017184", - "AB01000000020907", - "AB01000000017192", - "AB01000000016210", - "AB01000000016111", - "AB01000000016103", - "AB01000000015766", - "AB01000000015774", - "AB01000000015782", - "AB01000000022598", - "AB01000000022580", - "AB01000000005688", - "AB07000000005696", - "AB07000000005902", - "AB07000000005910", - "AB07000000005928", - "AB07000000005936", - "AB07000000005944", - "AB07000000005993", - "AB07000000005985", - "AB07000000005977", - "AB07000000005969", - "AB07000000005951", - "AB07000000005605", - "AB07000000005803", - "AB07000000005811", - "AB07000000005829", - "AB07000000005837", - "AB07000000005845", - "AB07000000005852", - "AB07000000005860", - "AB07000000005878", - "AB07000000005886", - "AB07000000005894", - "AB07000000005704", - "AB07000000005712", - "AB07000000005720", - "AB07000000005738", - "AB07000000005746", - "AB07000000005514", - "AB07000000005522", - "AB07000000005563", - "AB07000000005571", - "AB07000000005589", - "AB07000000005597", - "AB07000000005613", - "AB07000000005621", - "AB07000000005639", - "AB07000000005647", - "AB07000000005654", - "AB07000000005662", - "AB07000000005670", - "AB07000000005530", - "AB07000000005548", - "AB07000000005555", - "AB07000000005753", - "AB07000000005761", - "AB07000000005779", - "AB07000000005787", - "AB07000000005795", - "AB07000000005506", - - // Note ETH - "AB02000000051083", - "AB02000000051059", - "AB02000000051158", - "AB02000000050986", - "AB02000000051026", - "AB02000000050960", - "AB02000000051042", - "AB02000000051091", - "AB02000000051034", - "AB02000000051133", - "AB02000000019924", - "AB02000000019932", - "AB02000000022092", - "AB02000000022282", - "AB02000000023983", - "AB02000000023439", - "AB02000000020328", - "AB02000000020310", - "AB02000000021565", - "AB02000000022357", - "AB02000000023355", - "AB02000000022324", - "AB02000000022100", - "AB02000000019999", - "AB02000000020013", - "AB02000000020005", - "AB02000000020021", - "AB02000000020039", - "AB02000000020278", - "AB02000000020252", - "AB02000000018652", - "AB02000000018561", - "AB08000000009481", - "AB08000000009473", - "AB08000000009705", - "AB08000000009897", - "AB08000000009689", - "AB08000000009671", - "AB08000000009465", - "AB08000000009457", - "AB08000000009440", - "AB08000000009432", - "AB08000000009424", - "AB08000000009416", - "AB08000000009408", - "AB08000000009390", - "AB08000000009374", - "AB08000000009382", - "AB08000000009267", - "AB08000000009275", - "AB08000000009283", - "AB08000000009291", - "AB08000000009309", - "AB08000000009317", - "AB08000000009325", - "AB08000000009333", - "AB08000000009341", - "AB08000000009358", - "AB08000000009366", - "AB08000000009077", - "AB08000000009143", - "AB08000000009168", - "AB08000000009184", - "AB08000000009192", - "AB08000000009200", - "AB08000000009226", - "AB08000000009218", - "AB08000000009234", - "AB08000000009242", - "AB08000000008574", - "AB08000000009069", - "AB08000000008525", - "AB08000000009051", - "AB08000000009135", - "AB08000000009150", - "AB08000000009176", - "AB08000000009085", - "AB08000000009093", - "AB08000000009101", - "AB08000000009119", - "AB08000000009127", - "AB08000000009259", - - // === Technopark === - // Wallet - "AC01000000044120", - "AC01000000044997", - "AC01000000044989", - "AC01000000043494", - "AC01000000043486", - "AC01000000044187", - "AC01000000043148", - "AC01000000044013", - "AC01000000043973", - "AC01000000044815", - "AC01000000044807", - "AC01000000043809", - "AC01000000043833", - "AC01000000043460", - "AC01000000043064", - "AC01000000044138", - "AC01000000044500", - "AC01000000044492", - "AC01000000044260", - "AC01000000044278", - - // Note BTC - "AB01000000049864", - "AB01000000053239", - "AB01000000053056", - "AB01000000054237", - "AB01000000054245", - "AB01000000054211", - "AB01000000054229", - "AB01000000053189", - "AB01000000054195", - "AB01000000050797", - "AB01000000053833", - "AB01000000052124", - "AB01000000051605", - "AB01000000052223", - "AB01000000052207", - "AB01000000052199", - "AB01000000047785", - "AB01000000047850", - "AB01000000047868", - "AB01000000048288", - - // Note ETH - "AB02000000049715", - "AB02000000049848", - "AB02000000049814", - "AB02000000049863", - "AB02000000049871", - "AB02000000049855", - "AB02000000049285", - "AB02000000049277", - "AB02000000049558", - "AB02000000049889", - "AB02000000049988", - "AB02000000049707", - "AB02000000049699", - "AB02000000049897", - "AB02000000049905", - "AB02000000049913", - "AB02000000049251", - "AB02000000049533", - "AB02000000049541", - "AB02000000049830", - // === more cids === - "AC03000000091418", - "AC03000000091400", - "AC03000000099007", - "AC03000000098991", - "AC03000000098942", - "AC03000000091715", - "AC03000000091301", - "AC03000000091343", - "AB01000000055705", - "AB01000000052918", - "AB01000000047710", - "AB01000000052306", - "AB01000000047645", - "AB01000000048957", - "AB01000000052900", - "AB01000000050391", - "AB01000000047363", - "AB02000000053998", - "AB02000000019809", - "AB02000000020872", - "AB02000000022027", - "AB02000000058955", - "AB02000000053253", - "AB02000000048063", - "AB02000000023736", - "AB02000000058187", - ) - - @Suppress("ClassOrdering") - private val testDemoCardIds = listOf( - "FB20000000000186", // Note ETH - "FB10000000000196", // Note BTC - "FB30000000000176", // Wallet - ) - - @Suppress("ClassOrdering") - private val debugTestDemoCardIds = listOf() -} - class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { override suspend fun getFee(amount: Amount, destination: String): Result> { diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt index 9b391b22df..81fcc9fc13 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.demo.DemoConfig import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 89dc7ed09b..ab69008a6b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -68,7 +68,7 @@ class DetailsMiddleware { private fun handleAction(state: DetailsState, action: Action) { when (action) { is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) - is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) + is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state) is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) @@ -157,15 +157,15 @@ class DetailsMiddleware { class ManageSecurityMiddleware { @Suppress("ComplexMethod") - fun handle(action: DetailsAction.ManageSecurity) { + fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) { when (action) { is DetailsAction.ManageSecurity.OpenSecurity -> { store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) } is DetailsAction.ManageSecurity.SaveChanges -> { - val cardId = store.state.detailsState.scanResponse?.card?.cardId - val selectedOption = - store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption + val cardSettingsState = detailsState.cardSettingsState + val cardId = cardSettingsState?.card?.cardId + val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption scope.launch { val result = when (selectedOption) { SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index dd560af176..62179b433d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -5,11 +5,8 @@ import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState -import com.tangem.tap.store import org.rekotlin.StateType import java.util.* -import kotlin.properties.ReadOnlyProperty data class DetailsState( val scanResponse: ScanResponse? = null, @@ -19,14 +16,7 @@ data class DetailsState( val createBackupAllowed: Boolean = false, val appCurrency: FiatCurrency = FiatCurrency.Default, val appSettingsState: AppSettingsState = AppSettingsState(), -) : StateType { - - // if you do not delegate - the application crashes on startup, - // because twinCardsState has not been created yet - val twinCardsState: TwinCardsState by ReadOnlyProperty { _, _ -> - store.state.twinCardsState - } -} +) : StateType data class CardInfo( val cardId: String, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index c89c19485a..1580b87400 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -4,8 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater @@ -21,9 +19,6 @@ class DetailsFragment : Fragment(), StoreSubscriber { private val detailsViewModel = DetailsViewModel(store) - private var detailsScreenState: MutableState = - mutableStateOf(detailsViewModel.updateState(store.state.detailsState)) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Analytics.send(Settings.ScreenOpened()) @@ -42,7 +37,7 @@ class DetailsFragment : Fragment(), StoreSubscriber { isTransitionGroup = true TangemTheme { DetailsScreen( - state = detailsScreenState.value, + state = detailsViewModel.detailsScreenState.value, onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, ) } @@ -66,6 +61,6 @@ class DetailsFragment : Fragment(), StoreSubscriber { override fun newState(state: DetailsState) { if (activity == null || view == null) return - detailsScreenState.value = detailsViewModel.updateState(state) + detailsViewModel.detailsScreenState.value = detailsViewModel.updateState(state) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index c80a04b785..631f292c66 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -2,6 +2,8 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -16,8 +18,13 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource @@ -27,10 +34,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.compose.TangemTypography import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R +import kotlinx.coroutines.launch @Composable fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { @@ -46,32 +55,35 @@ fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { @Composable fun Content(state: DetailsScreenState) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) - state.elements.map { element -> - if (element == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemsClick = state.onItemsClick) - } else { - DetailsItem( - item = element, - appCurrency = state.appCurrency, - onItemsClick = { state.onItemsClick(element) }, - ) + Box { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) + state.elements.map { element -> + if (element == SettingsElement.WalletConnect) { + WalletConnectDetailsItem(onItemsClick = state.onItemsClick) + } else { + DetailsItem( + item = element, + appCurrency = state.appCurrency, + onItemsClick = { state.onItemsClick(element) }, + ) + } } + Spacer(modifier = Modifier.weight(1f)) + TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", + style = TangemTypography.caption, + color = colorResource(id = R.color.text_tertiary), + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + ) } - Spacer(modifier = Modifier.weight(1f)) - TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", - style = TangemTypography.caption, - color = colorResource(id = R.color.text_tertiary), - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), - ) + ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @@ -165,6 +177,36 @@ fun TangemSocialAccounts( } } +@Composable +fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + SnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + hostState = snackbarHostState, + ) + val errorTitle = when (snackbarErrorState) { + is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled) + EventError.Empty -> "" + } + if (snackbarErrorState != EventError.Empty) { + SideEffect { + coroutineScope.launch { + snackbarHostState.showSnackbar(errorTitle) + } + when (snackbarErrorState) { + is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke() + else -> { + /*no-op*/ + } + } + } + } +} + @Composable @Preview private fun Preview() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index 88d83b897c..df8b8979be 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import com.tangem.wallet.R @Immutable @@ -11,6 +13,7 @@ data class DetailsScreenState( val appCurrency: String, val onItemsClick: (SettingsElement) -> Unit, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, + val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), ) { val appNameRes: Int = R.string.tangem_app_name } @@ -29,7 +32,8 @@ enum class SettingsElement( AppSettings(R.drawable.ic_settings, R.string.app_settings_title), LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App - PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy); + PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), + TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu) } @Immutable @@ -38,6 +42,11 @@ data class SocialNetworkLink( val url: String, ) +sealed class EventError { + object Empty : EventError() + data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError() +} + sealed class SocialNetwork(val id: String, val iconRes: Int) { object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 051a9148f2..49de80618a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -1,5 +1,7 @@ package com.tangem.tap.features.details.ui.details +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.feedback.FeedbackEmail @@ -8,6 +10,7 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider @@ -18,6 +21,9 @@ import org.rekotlin.Store class DetailsViewModel(private val store: Store) { + var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) + private set + @Suppress("ComplexMethod") fun updateState(state: DetailsState): DetailsScreenState { val cardTypesResolver = state.scanResponse?.cardTypesResolver @@ -39,6 +45,7 @@ class DetailsViewModel(private val store: Store) { SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null + SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null else -> it } } @@ -94,7 +101,18 @@ class DetailsViewModel(private val store: Store) { // TODO: To be available later } SettingsElement.ReferralProgram -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + if (store.state.detailsState.scanResponse?.let { DemoHelper.isDemoCard(it) } == true) { + detailsScreenState.value.showErrorSnackbar.value = EventError.DemoReferralNotAvailable( + onErrorShow = { + detailsScreenState.value.showErrorSnackbar.value = EventError.Empty + }, + ) + } else { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + } + } + SettingsElement.TesterMenu -> { + store.state.daggerGraphState.testerRouter?.startTesterScreen() } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index ac58136a05..c552a354a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback -import com.tangem.datasource.utils.AssetReader +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index f471878c85..4c8d89c813 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -15,10 +15,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback import com.tangem.core.analytics.Analytics import com.tangem.core.ui.fragments.setStatusBarColor -import com.tangem.datasource.utils.AndroidAssetReader +import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TwinCardNumber -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.getDrawableCompat @@ -39,10 +39,16 @@ import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @Suppress("LargeClass") +@AndroidEntryPoint class TwinsCardsFragment : BaseOnboardingFragment() { + @Inject + lateinit var assetReader: AssetReader + private val mainBinding by lazy { binding.vMain } private var previousStep: TwinCardsStep = TwinCardsStep.None @@ -253,8 +259,8 @@ class TwinsCardsFragment : BaseOnboardingFragment() { Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) store.dispatch( TwinCardsAction.Wallet.LaunchFirstStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - AndroidAssetReader(requireContext()), + initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + reader = assetReader, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 7125990ede..07939d2ad2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -19,8 +19,8 @@ import com.google.android.material.tabs.TabLayoutMediator import com.tangem.common.CardIdFormatter import com.tangem.common.core.CardIdDisplayFormat import com.tangem.core.analytics.Analytics -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.inflate diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt index 7385c4d025..64479ca66d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt @@ -6,9 +6,9 @@ import android.animation.PropertyValuesHolder import android.view.View import android.widget.ImageView import androidx.core.animation.doOnEnd -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState -import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState +import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget class WalletCardsWidget( val leapfrogWidget: LeapfrogWidget, diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 64a01578f1..f69e50bf9c 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -155,6 +155,12 @@ sealed class SendAction : SendScreenAction { val reduceAmount: BigDecimal, ) : Dialog() + data class KaspaWarningDialog( + val maxOutputs: Int, + val maxAmount: BigDecimal, + val onOk: () -> Unit, + ) : Dialog() + sealed class SendTransactionFails : Dialog() { data class CardSdkError(val error: TangemSdkError) : Dialog() data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog() diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt index 7a94dce2a8..81d62770bc 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt @@ -52,7 +52,7 @@ internal class AddressPayIdMiddleware { } private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) { - val potentialPayId = data.toLowerCase() + val potentialPayId = data.lowercase() if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) { dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput)) } else { @@ -113,10 +113,11 @@ internal class AddressPayIdMiddleware { } private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) { - val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) { - listOf(address) - } else { - address.split(":") + val addressSchemeSplit = when (wallet.blockchain) { + Blockchain.BitcoinCash, + Blockchain.Kaspa, + -> listOf(address) + else -> address.split(":") } val noSchemeAddress = when (addressSchemeSplit.size) { 1 -> address // no scheme diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 00d9ef32c1..b87cc3c49b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -269,6 +269,18 @@ private fun sendTransaction( // from XLM, XRP, Polkadot dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) } + is BlockchainSdkError.Kaspa.UtxoAmountError -> { + dispatch( + SendAction.Dialog.KaspaWarningDialog( + maxOutputs = error.maxOutputs, + maxAmount = error.maxAmount, + onOk = { + dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false)) + dispatch(AmountActionUi.CheckAmountToSend) + }, + ), + ) + } else -> { when { error.customMessage.contains(DemoTransactionSender.ID) -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index a0d911ccc8..a26e390555 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -55,6 +55,7 @@ private class SendReducer : SendInternalReducer { sendState.copy(sendButtonState = IndeterminateProgressButton(action.state)) } is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action) + is SendAction.Dialog.KaspaWarningDialog -> sendState.copy(dialog = action) is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action) is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action) is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 55b1211cfb..ecc6d5d00e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -17,7 +17,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message import com.tangem.core.analytics.Analytics -import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard +import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.entities.FiatCurrency diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt new file mode 100644 index 0000000000..0b60a105f5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.send.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.store +import com.tangem.wallet.R + +object KaspaWarningDialog { + fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.common_warning) + setMessage( + context.getString( + R.string.kaspa_withdrawal_message_warning, + dialog.maxOutputs, + dialog.maxAmount.toPlainString(), + ), + ) + setPositiveButton(R.string.common_ok) { _, _ -> + store.dispatch(SendAction.Dialog.Hide) + dialog.onOk() + } + } + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 600e7a99d7..8d1fe0182f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -5,7 +5,7 @@ import androidx.appcompat.app.AlertDialog import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.module.ModuleMessageConverter -import com.tangem.tangem_sdk_new.extensions.localizedDescription +import com.tangem.sdk.extensions.localizedDescription import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.global.GlobalAction diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index ac418ad22c..055f885169 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -33,6 +33,7 @@ import com.tangem.tap.features.send.redux.states.TransactionExtraError import com.tangem.tap.features.send.redux.states.TransactionExtrasState import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.send.ui.dialogs.KaspaWarningDialog import com.tangem.tap.features.send.ui.dialogs.RequestFeeErrorDialog import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog @@ -124,6 +125,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : dialog?.show() } } + is SendAction.Dialog.KaspaWarningDialog -> { + if (dialog == null) { + dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog) + dialog?.show() + } + } is SendAction.Dialog.SendTransactionFails.CardSdkError -> { if (dialog == null) { dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 0896404d09..305df3393e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -21,7 +21,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE -import com.tangem.tap.assetReader import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain @@ -32,6 +31,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.tokens.LoadAvailableCoinsService import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -85,7 +85,7 @@ class TokensMiddleware { val loadCoinsService = LoadAvailableCoinsService( tangemTechApi = store.state.domainNetworks.tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), - assetReader = assetReader, + assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader), ) scope.launch { @@ -264,14 +264,10 @@ class TokensMiddleware { val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } if (toDerive.isEmpty()) return null - return DerivationData( - derivations = mapKeyOfWalletPublicKey to toDerive, - ) + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } - private class DerivationData( - val derivations: Pair>, - ) + private class DerivationData(val derivations: Pair>) private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt index f50513a1ae..e4e5f8e78c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.common.extensions.isZero import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction @@ -36,22 +37,26 @@ data class WalletData( return exchangeManager.availableForSell(currency) } - fun isAvailableToSwap(swapInteractor: SwapInteractor, isExchangeFeatureOn: Boolean): Boolean { + fun isAvailableToSwap( + swapFeatureToggleManager: SwapFeatureToggleManager, + swapInteractor: SwapInteractor, + ): Boolean { + if (currency.blockchain.id == Blockchain.Optimism.id && !swapFeatureToggleManager.isOptimismSwapEnabled) { + return false + } return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) && - currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now - isExchangeFeatureOn && !currency.isCustomCurrency(null) } fun getAvailableActions( swapInteractor: SwapInteractor, exchangeManager: CurrencyExchangeManager, - isExchangeFeatureOn: Boolean, + swapFeatureToggleManager: SwapFeatureToggleManager, ): Set { return setOfNotNull( if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null, if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null, - if (isAvailableToSwap(swapInteractor, isExchangeFeatureOn)) CurrencyAction.Swap else null, + if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor)) CurrencyAction.Swap else null, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 5d12f94a9a..b0d8086014 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.AmountType import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics +import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.MainScreen @@ -34,15 +35,14 @@ import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.redux.reducers.findSelectedCurrency -import com.tangem.tap.network.NetworkConnectivity -import com.tangem.tap.network.NetworkStateChanged import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.tap.totalFiatBalanceCalculator import com.tangem.tap.userWalletsListManager -import com.tangem.tap.userWalletsListManagerSafe +import com.tangem.tap.walletStoresManager import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -68,6 +68,9 @@ class WalletMiddleware { ) } + private val networkConnectionManager: NetworkConnectionManager + get() = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + val walletMiddleware: Middleware = { _, state -> { next -> { action -> @@ -131,16 +134,6 @@ class WalletMiddleware { ) } } - is NetworkStateChanged -> { - store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) - if (!action.isOnline) return - - val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard { - Timber.e("Unable to proceed with changed network state, no user wallet selected") - return - } - scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) } - } is WalletAction.CopyAddress -> { Analytics.send(Token.Receive.ButtonCopyAddress()) action.context.copyToClipboard(action.address) @@ -155,7 +148,7 @@ class WalletMiddleware { store.dispatchOpenUrl(action.exploreUrl) } is WalletAction.Send -> { - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + if (!networkConnectionManager.isOnline) { store.dispatchErrorNotification(TapError.NoInternetConnection) return } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index d9a40e965d..1de96bac4b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -2,29 +2,26 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.SignatureCountValidator -import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.isGreaterThan import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.extensions.hasSignedHashes import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal class WarningsMiddleware { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { @@ -75,13 +72,6 @@ class WarningsMiddleware { } } - fun tryToShowAppRatingWarning(wallet: Wallet) { - val nonZeroWalletsCount = wallet.amounts.filter { - it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false - }.size - tryToShowAppRatingWarning(hasNonZeroWallets = nonZeroWalletsCount > 0) - } - private fun showCardWarningsIfNeeded(globalState: GlobalState?) { globalState?.scanResponse?.let { scanResponse -> val card = scanResponse.card @@ -147,7 +137,9 @@ class WarningsMiddleware { private fun checkHashesCountOnline() { if (store.state.walletState.hashesCountVerified != false) return - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return + + val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + if (!networkConnectionManager.isOnline) return val scanResponse = store.state.globalState.scanResponse val card = scanResponse?.card diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index af465cf7b2..2668f1c79a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -24,8 +24,9 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.sdk.extensions.dpToPx import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.TestActions import com.tangem.tap.common.analytics.events.DetailsScreen @@ -75,6 +76,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), @Inject lateinit var swapInteractor: SwapInteractor + @Inject + lateinit var swapFeatureToggleManager: SwapFeatureToggleManager + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter @@ -290,7 +294,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), WalletAction.DialogAction.ChooseTradeActionDialog( buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager), sellAllowed = selectedWallet.isAvailableToSell(exchangeManager), - swapAllowed = selectedWallet.isAvailableToSwap(swapInteractor, isExchangeServiceFeatureOn), + swapAllowed = selectedWallet.isAvailableToSwap(swapFeatureToggleManager, swapInteractor), ), ) } @@ -298,7 +302,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), val actions = selectedWallet.getAvailableActions( swapInteractor = swapInteractor, exchangeManager = exchangeManager, - isExchangeFeatureOn = isExchangeServiceFeatureOn, + swapFeatureToggleManager = swapFeatureToggleManager, ) binding.rowButtons.updateButtonsVisibility( actions = actions, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index f3a6eba304..dc76e27d7c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -21,6 +21,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.utils.OneTouchClickListener +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.MainActivity import com.tangem.tap.common.analytics.events.Portfolio @@ -56,6 +57,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber totalBalance?.state?.let { + viewModel.onBalanceLoaded(totalBalance) store.state.globalState.topUpController?.totalBalanceStateChanged(it) } } @@ -75,7 +80,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) - store.state.globalState.scanResponse?.let { scanNoteResponse -> + store.state.globalState.scanResponse?.let { scanResponse -> store.dispatch( DetailsAction.PrepareScreen( - scanResponse = scanNoteResponse, + scanResponse = scanResponse, wallets = store.state.walletState.walletManagers.map { it.wallet }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt index 10ac5bdca7..c77510365e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt @@ -4,15 +4,18 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.features.wallet.models.TotalBalance import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper import com.tangem.tap.store import com.tangem.tap.walletStoresManager +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -22,16 +25,22 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber +import javax.inject.Inject // TODO: Kill me, please @OptIn(ExperimentalCoroutinesApi::class) -internal class WalletViewModel : ViewModel(), StoreSubscriber, DefaultLifecycleObserver { +@HiltViewModel +internal class WalletViewModel @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel(), StoreSubscriber, DefaultLifecycleObserver { private var observeWalletStoresUpdatesJob: Job? = null set(value) { field?.cancel() field = value } + private val walletAnalyticsEventsMapper = WalletAnalyticsEventsMapper() + init { subscribeToUserWalletsListManagerUpdates() } @@ -53,7 +62,7 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber + analyticsEventHandler.send( + Basic.BalanceLoaded( + balance = balanceParam, + ), + ) + } + } + } + private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) { observeWalletStoresUpdatesJob = manager.selectedUserWallet .map { it.walletId } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/analytics/WalletAnalyticsEventsMapper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/analytics/WalletAnalyticsEventsMapper.kt new file mode 100644 index 0000000000..6c750a5368 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/analytics/WalletAnalyticsEventsMapper.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.features.wallet.ui.analytics + +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.extensions.isGreaterThan +import com.tangem.tap.features.wallet.models.TotalBalance +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +class WalletAnalyticsEventsMapper : Converter { + + override fun convert(value: TotalBalance): AnalyticsParam.CardBalanceState? { + return when (value.state) { + ProgressState.Done -> if (value.fiatAmount?.isGreaterThan(BigDecimal.ZERO) == true) { + AnalyticsParam.CardBalanceState.Full + } else { + AnalyticsParam.CardBalanceState.Empty + } + ProgressState.Error -> { + if (value.fiatAmount == null) { + // if fiatAmount is null while ProgressState.Error it means error occurs when loading blockchain + AnalyticsParam.CardBalanceState.BlockchainError + } else { + // if fiatAmount is not null while ProgressState.Error it means couldn't load custom token amount + AnalyticsParam.CardBalanceState.CustomToken + } + } + else -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt index d05ac08cc9..4019726797 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconView.kt @@ -8,7 +8,7 @@ import androidx.constraintlayout.utils.widget.ImageFilterView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import com.tangem.blockchain.common.DerivationStyle -import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.sdk.extensions.dpToPx import com.tangem.tap.features.wallet.models.Currency import com.tangem.wallet.databinding.ViewCurrencyIconBinding import kotlin.math.roundToInt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index ed966ec0ae..f29df37ca0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -139,6 +139,7 @@ class SingleWalletView : WalletView() { isExchangeServiceFeatureEnabled: Boolean, ) { val swapInteractor = this.swapInteractor ?: return + val swapFeatureToggleManager = this.swapFeatureToggleManager ?: return val exchangeManager = store.state.globalState.exchangeManager binding?.rowButtons?.apply { @@ -158,7 +159,7 @@ class SingleWalletView : WalletView() { val actions = walletData.getAvailableActions( swapInteractor = swapInteractor, exchangeManager = exchangeManager, - isExchangeFeatureOn = isExchangeServiceFeatureEnabled, + swapFeatureToggleManager = swapFeatureToggleManager, ) binding?.rowButtons?.updateButtonsVisibility( actions = actions, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt index 27b263ae5e..c3cb5a7c74 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.ui.wallet +import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.WalletFragment @@ -8,6 +9,7 @@ import com.tangem.wallet.databinding.FragmentWalletBinding abstract class WalletView { var swapInteractor: SwapInteractor? = null + var swapFeatureToggleManager: SwapFeatureToggleManager? = null protected var fragment: WalletFragment? = null protected var binding: FragmentWalletBinding? = null diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt index 4e8b5ec2d6..b41635abb0 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt @@ -35,6 +35,7 @@ internal sealed interface WalletSelectorAction : Action { data class SelectWallet( val userWalletId: UserWalletId, + val sendAnalyticsEvent: Boolean = false, ) : WalletSelectorAction data class RenameWallet( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index bfd8936b17..c93b07f234 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -70,7 +70,7 @@ internal class WalletSelectorMiddleware { addWallet() } is WalletSelectorAction.SelectWallet -> { - selectWallet(action.userWalletId) + selectWallet(action.userWalletId, action.sendAnalyticsEvent) } is WalletSelectorAction.RemoveWallets -> { deleteWallets(action.userWalletsIds, state) @@ -189,7 +189,7 @@ internal class WalletSelectorMiddleware { } } - private fun selectWallet(userWalletId: UserWalletId) { + private fun selectWallet(userWalletId: UserWalletId, sendAnalyticsEvent: Boolean) { scope.launch { userWalletsListManager.get(userWalletId) .flatMap { userWallet -> @@ -212,7 +212,10 @@ internal class WalletSelectorMiddleware { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) - store.onUserWalletSelected(selectedUserWallet) + store.onUserWalletSelected( + userWallet = selectedUserWallet, + sendAnalyticsEvent = true, + ) } } } @@ -336,7 +339,10 @@ internal class WalletSelectorMiddleware { store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) } currentSelectedWalletId != selectedUserWallet.walletId -> { - store.onUserWalletSelected(selectedUserWallet) + store.onUserWalletSelected( + userWallet = selectedUserWallet, + sendAnalyticsEvent = true, + ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index dbb367554c..b611289180 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -32,7 +32,9 @@ import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogCo import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent import com.tangem.tap.features.walletSelector.ui.model.DialogModel import com.tangem.tap.features.walletSelector.ui.model.WarningModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() { private val viewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt index 9384a5f22b..dee873d6ce 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -54,7 +54,12 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber { - store.dispatch(WalletSelectorAction.SelectWallet(userWalletId)) + store.dispatch( + WalletSelectorAction.SelectWallet( + userWalletId = userWalletId, + sendAnalyticsEvent = true, + ), + ) } } } diff --git a/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt b/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt deleted file mode 100644 index bf61064eaa..0000000000 --- a/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.tap.network - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.ConnectivityManager -import android.net.NetworkCapabilities -import android.os.Build -import org.rekotlin.Action -import org.rekotlin.Store -import java.lang.ref.WeakReference - -/** -[REDACTED_AUTHOR] - */ -@Deprecated("Use com.tangem.datasource.connection.NetworkConnectionManager") -class NetworkConnectivity( - private val store: Store<*>, - context: Context, -) { - - private val wContext: WeakReference = WeakReference(context) - - private val receiver = object : BroadcastReceiver() { - override fun onReceive(c: Context?, intent: Intent?) { - store.dispatch(NetworkStateChanged(isOnlineOrConnecting())) - } - } - - init { - val intentFilter = IntentFilter() - intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) - wContext.get()?.registerReceiver(receiver, intentFilter) - store.dispatch(NetworkStateChanged(isOnlineOrConnecting())) - } - - fun isOnlineOrConnecting(): Boolean { - val connectivityManager = getConnectivityManager() ?: return false - - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - capabilities != null && - (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) - } else { - val networkInfo = connectivityManager.activeNetworkInfo - networkInfo != null && networkInfo.isConnectedOrConnecting - } - } - - private fun getConnectivityManager(): ConnectivityManager? { - return wContext.get()?.applicationContext - ?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager - } - - companion object { - private lateinit var instance: NetworkConnectivity - - fun createInstance(store: Store<*>, context: Context): NetworkConnectivity { - instance = NetworkConnectivity(store, context) - return instance - } - - fun getInstance(): NetworkConnectivity = instance - } -} - -data class NetworkStateChanged(val isOnline: Boolean) : Action \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index 4fc1c8bfb4..705cd2824f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -4,6 +4,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath @@ -13,6 +15,7 @@ import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NonNativeToken +import com.tangem.lib.crypto.models.errors.UserCancelledException import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -49,9 +52,10 @@ class DerivationManagerImpl( if (scanResponse != null) { deriveMissingBlockchains( scanResponse = scanResponse, - listOf(appCurrency), + currencyList = listOf(appCurrency), + onSuccess = { continuation.resumeWith(Result.success(true)) }, ) { - continuation.resumeWith(Result.success(true)) + continuation.resumeWith(Result.failure(it)) } } } else { @@ -84,6 +88,7 @@ class DerivationManagerImpl( scanResponse: ScanResponse, currencyList: List, onSuccess: (ScanResponse) -> Unit, + onFailure: (Exception) -> Unit, ) { val derivationDataList = listOfNotNull( getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), @@ -135,6 +140,7 @@ class DerivationManagerImpl( "Error derivation", ), ) + onFailure.invoke(handleTangemError(result.error)) } else -> { error("result result is null") @@ -176,6 +182,19 @@ class DerivationManagerImpl( ) } + /** + * Simple error handler + * for now specifically handle only UserCancelled + * + * @param error [TangemError] + */ + private fun handleTangemError(error: TangemError): Exception { + if (error is TangemSdkError.UserCancelled) { + return UserCancelledException() + } + return IllegalStateException(error.customMessage) + } + private class DerivationData( val derivations: Pair>, ) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 6ec7b292bb..b5225f4588 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -4,6 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.Message import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError @@ -155,56 +156,31 @@ class TransactionManagerImpl( val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) if (walletManager is EthereumWalletManager) { - val gasLimit = getGasLimit( - evmWalletManager = walletManager, + if (walletManager is OptimismWalletManager) { + return getFeeForOptimismBlockchain( + walletManager = walletManager, + amount = createAmount(amountToSend, currencyToSend, blockchain), + destinationAddress = destinationAddress, + data = data, + ) + } + return getFeeForEthereumBlockchain( + walletManager = walletManager, blockchain = blockchain, - amount = amountToSend, + amountToSend = amountToSend, currency = currencyToSend, destinationAddress = destinationAddress, data = data, - ).let { - if (increaseBy != null && increaseBy != 0) { - it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100")) - } else { - it - } - } - return when (val gasPrice = walletManager.getGasPrice()) { - is Result.Success -> { - val fee = gasLimit.multiply(gasPrice.data).toBigDecimal( - scale = blockchain.decimals(), - mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), - ) - ProxyFee( - gasLimit = gasLimit, - fee = ProxyAmount( - currencySymbol = blockchain.currency, - value = fee, - decimals = blockchain.decimals(), - ), - ) - } - is Result.Failure -> { - error(gasPrice.error.message ?: gasPrice.error.customMessage) - } - } + increaseBy = increaseBy, + ) } else { - val fee = (walletManager as? TransactionSender)?.getFee( - amount = createAmount(amountToSend, currencyToSend, blockchain), - destination = destinationAddress, - ) ?: error("Cannot cast to TransactionSender") - when (fee) { - is Result.Success -> { - // for not EVM blockchains set gasLimit ZERO for now - return ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")), - ) - } - is Result.Failure -> { - error(fee.error.message ?: fee.error.customMessage) - } - } + return getFeeForBlockchain( + walletManager = walletManager, + amountToSend = amountToSend, + currency = currencyToSend, + blockchain = blockchain, + destinationAddress = destinationAddress, + ) } } @@ -217,6 +193,100 @@ class TransactionManagerImpl( ) } + private suspend fun getFeeForBlockchain( + walletManager: WalletManager, + amountToSend: BigDecimal, + currency: Currency, + blockchain: Blockchain, + destinationAddress: String, + ): ProxyFee { + val fee = (walletManager as? TransactionSender)?.getFee( + amount = createAmount(amountToSend, currency, blockchain), + destination = destinationAddress, + ) ?: error("Cannot cast to TransactionSender") + return when (fee) { + is Result.Success -> { + // for not EVM blockchains set gasLimit ZERO for now + ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")), + ) + } + is Result.Failure -> { + error(fee.error.message ?: fee.error.customMessage) + } + } + } + + @Suppress("LongParameterList") + private suspend fun getFeeForEthereumBlockchain( + walletManager: EthereumWalletManager, + blockchain: Blockchain, + amountToSend: BigDecimal, + currency: Currency, + destinationAddress: String, + data: String?, + increaseBy: Int?, + ): ProxyFee { + val gasLimit = getGasLimit( + evmWalletManager = walletManager, + blockchain = blockchain, + amount = amountToSend, + currency = currency, + destinationAddress = destinationAddress, + data = data, + ).let { + if (increaseBy != null && increaseBy != 0) { + it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100")) + } else { + it + } + } + return when (val gasPrice = walletManager.getGasPrice()) { + is Result.Success -> { + val fee = gasLimit.multiply(gasPrice.data).toBigDecimal( + scale = blockchain.decimals(), + mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), + ) + ProxyFee( + gasLimit = gasLimit, + fee = ProxyAmount( + currencySymbol = blockchain.currency, + value = fee, + decimals = blockchain.decimals(), + ), + ) + } + is Result.Failure -> { + error(gasPrice.error.message ?: gasPrice.error.customMessage) + } + } + } + + private suspend fun getFeeForOptimismBlockchain( + walletManager: OptimismWalletManager, + amount: Amount, + destinationAddress: String, + data: String?, + ): ProxyFee { + val fee = if (data.isNullOrEmpty()) { + walletManager.getFee(amount, destinationAddress) + } else { + walletManager.getFee(amount, destinationAddress, data) + } + when (fee) { + is Result.Success -> { + return ProxyFee( + gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, + fee = convertToProxyAmount(fee.data.lastOrNull() ?: error("no fee found")), + ) + } + is Result.Failure -> { + error(fee.error.message ?: fee.error.customMessage) + } + } + } + @Suppress("LongParameterList") private suspend fun getGasLimit( evmWalletManager: EthereumWalletManager, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt new file mode 100644 index 0000000000..cd392ae831 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -0,0 +1,16 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.features.tester.api.TesterRouter +import org.rekotlin.Action + +sealed interface DaggerGraphAction : Action { + + data class SetApplicationDependencies( + val assetReader: AssetReader, + val networkConnectionManager: NetworkConnectionManager, + ) : DaggerGraphAction + + data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt new file mode 100644 index 0000000000..ee884e4e2a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Middleware + +object DaggerGraphMiddleware { + val daggerGraphMiddleware: Middleware = { _, _ -> + { next -> + { action -> next(action) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt new file mode 100644 index 0000000000..34564599ac --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +object DaggerGraphReducer { + fun reduce(action: Action, state: AppState): DaggerGraphState { + if (action !is DaggerGraphAction) return state.daggerGraphState + + return internalReduce(action, state) + } + + private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { + return when (action) { + is DaggerGraphAction.SetApplicationDependencies -> state.daggerGraphState.copy( + assetReader = action.assetReader, + networkConnectionManager = action.networkConnectionManager, + ) + is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( + testerRouter = action.testerRouter, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt new file mode 100644 index 0000000000..800182a67c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.features.tester.api.TesterRouter +import org.rekotlin.StateType + +data class DaggerGraphState( + val assetReader: AssetReader? = null, + val testerRouter: TesterRouter? = null, + val networkConnectionManager: NetworkConnectionManager? = null, +) : StateType { + + inline fun get(getDependency: DaggerGraphState.() -> T?): T { + return requireNotNull(getDependency()) { + "${T::class.simpleName} isn't initialized " + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_kaspa_no_color.xml b/app/src/main/res/drawable/ic_kaspa_no_color.xml new file mode 100644 index 0000000000..cae1a10de8 --- /dev/null +++ b/app/src/main/res/drawable/ic_kaspa_no_color.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index d5f3e649d4..454c57b5fd 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -1,13 +1,5 @@ - - - - - - - - api.tangem.com @@ -17,5 +9,4 @@ - diff --git a/build.gradle.kts b/build.gradle.kts index f71b201932..aa62254f1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,37 +1,13 @@ -buildscript { - repositories { - google() - mavenCentral() - maven(url = "https://maven.fabric.io/public") - } - - dependencies { - classpath(ClasspathDependency.AndroidGradlePlugin) - classpath(ClasspathDependency.KotlinGradlePlugin) - classpath(ClasspathDependency.AndroidMavenGradlePlugin) - classpath(ClasspathDependency.GoogleServices) - classpath(ClasspathDependency.GoogleFirebaseCrashlytics) - } -} - plugins { - id("com.google.dagger.hilt.android") version "2.44" apply false - kotlin("plugin.serialization") version Versions.kotlin apply false -} - -allprojects { - repositories { - google() - jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview - mavenLocal() - maven("https://nexus.tangem-tech.com/repository/maven-releases/") - maven("https://jitpack.io") - maven("https://zendesk.jfrog.io/zendesk/repo") - } -} - -subprojects { - apply(plugin = "detekt-convention") + alias(deps.plugins.kotlin.android) apply false + alias(deps.plugins.kotlin.jvm) apply false + alias(deps.plugins.kotlin.serialization) apply false + alias(deps.plugins.kotlin.kapt) apply false + alias(deps.plugins.android.application) apply false + alias(deps.plugins.android.library) apply false + alias(deps.plugins.hilt.android) apply false + alias(deps.plugins.google.services) apply false + alias(deps.plugins.firebase.crashlytics) apply false } tasks.register("clean", Delete::class) { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index af3fd561b4..0000000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - `kotlin-dsl` -} - -repositories { - google() - mavenCentral() -} - -dependencies { - implementation("com.android.tools.build:gradle:7.1.3") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10") - //TODO("Upgrade version to 1.22.0 after upgrading kotlin version to 1.7.21") - implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0") - implementation("com.squareup:javapoet:1.13.0") -} \ No newline at end of file diff --git a/buildSrc/src/main/java/ClasspathDependency.kt b/buildSrc/src/main/java/ClasspathDependency.kt deleted file mode 100644 index 23fcf71450..0000000000 --- a/buildSrc/src/main/java/ClasspathDependency.kt +++ /dev/null @@ -1,9 +0,0 @@ -object ClasspathDependency { - const val AndroidGradlePlugin = "com.android.tools.build:gradle:" + Versions.androidGradlePlugin - const val AndroidMavenGradlePlugin = "com.github.dcendents:android-maven-gradle-plugin:" + - Versions.androidMavenGradlePlugin - const val GoogleFirebaseCrashlytics = "com.google.firebase:firebase-crashlytics-gradle:" + - Versions.googleFirebaseCrashlytics - const val GoogleServices = "com.google.gms:google-services:" + Versions.googleServices - const val KotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + Versions.kotlin -} \ No newline at end of file diff --git a/buildSrc/src/main/java/ConfigFieldBuilder.kt b/buildSrc/src/main/java/ConfigFieldBuilder.kt deleted file mode 100644 index 42c1f16c56..0000000000 --- a/buildSrc/src/main/java/ConfigFieldBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -typealias ConfigFieldBuilder = (String, String, String) -> Unit - -class BuildConfigFieldFactory( - private val fields: List, - private val builder: ConfigFieldBuilder, -) { - fun create() { - fields.forEach { field -> builder(field.type, field.name, field.value) } - } -} - -/** - * Build config field - * - * @property type field type - * @property name field name - * @property value field value - */ -sealed class Field(val type: String, val name: String, val value: String) { - - class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"") - - class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString()) - - class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString()) -} \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt deleted file mode 100644 index eaf82306b2..0000000000 --- a/buildSrc/src/main/java/Dependency.kt +++ /dev/null @@ -1,91 +0,0 @@ -object AndroidX { - const val activityCompose = "androidx.activity:activity-compose:" + Versions.androidxActivityCompose - const val appCompat = "androidx.appcompat:appcompat:" + Versions.androidxAppCompat - const val browser = "androidx.browser:browser:" + Versions.androidxBrowser - const val constraintLayout = "androidx.constraintlayout:constraintlayout:" + Versions.androidxConstraintLayout - const val coreKtx = "androidx.core:core-ktx:" + Versions.androidxCore - const val fragmentKtx = "androidx.fragment:fragment-ktx:" + Versions.androidxFragment - const val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:" + Versions.androidxLifecycle - const val lifecycleLiveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:" + Versions.androidxLifecycle - const val lifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:" + Versions.androidxLifecycle - const val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:" + Versions.androidxLifecycle -} - -object Compose { - const val animation = "androidx.compose.animation:animation:" + Versions.compose - const val coil = "io.coil-kt:coil-compose:" + Versions.coil - const val foundation = "androidx.compose.foundation:foundation:" + Versions.composeFoundation - const val material = "androidx.compose.material:material:" + Versions.compose - const val ui = "androidx.compose.ui:ui:" + Versions.compose - const val uiTooling = "androidx.compose.ui:ui-tooling:" + Versions.compose - const val constraintLayout = "androidx.constraintlayout:constraintlayout-compose:" + Versions.composeConstraint -} - -object Firebase { - const val bom = "com.google.firebase:firebase-bom:" + Versions.firebase - const val firebaseAnalytics = "com.google.firebase:firebase-analytics-ktx" - const val firebaseCrashlytics = "com.google.firebase:firebase-crashlytics-ktx" -} - -object Library { - const val accompanistAppCompatTheme = "com.google.accompanist:accompanist-appcompat-theme:" + Versions.accompanist - const val accompanistSystemUiController = - "com.google.accompanist:accompanist-systemuicontroller:" + Versions.accompanist - const val accompanistWebView = "com.google.accompanist:accompanist-webview:" + Versions.accompanist - const val amplitude = "com.amplitude:android-sdk:" + Versions.amplitude - const val appsflyer = "com.appsflyer:af-android-sdk:" + Versions.appsflyer - const val armadillo = "at.favre.lib:armadillo:" + Versions.armadillo - const val coil = "io.coil-kt:coil:" + Versions.coil - const val composeShimmer = "com.valentinilk.shimmer:compose-shimmer:" + Versions.composeShimmer - const val coroutine = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" + Versions.coroutine - const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:" + Versions.desugarJdkLibs - const val googlePlayCore = "com.google.android.play:core:" + Versions.googlePlayCore - const val googlePlayCoreKtx = "com.google.android.play:core-ktx:" + Versions.googlePlayCoreKtx - const val googlePlayServicesWallet = - "com.google.android.gms:play-services-wallet:" + Versions.googlePlayServicesWallet - const val hilt = "com.google.dagger:hilt-android:" + Versions.hilt - const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt - const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt - const val jodatime = "joda-time:joda-time:" + Versions.jodatime - const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt - const val lottie = "com.airbnb.android:lottie:" + Versions.lottie - const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent - const val moshi = "com.squareup.moshi:moshi:" + Versions.moshi - const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:" + Versions.moshi - const val okHttp = "com.squareup.okhttp3:okhttp:" + Versions.okhttp - const val okHttpLogging = "com.squareup.okhttp3:logging-interceptor:" + Versions.okhttp - const val otaliastudiosCameraView = "com.otaliastudios:cameraview:" + Versions.otaliastudiosCameraView - const val shopifyBuySdk = "com.shopify.mobilebuysdk:buy3:" + Versions.shopifyBuySdk - const val spongecastleCryptoCore = "com.madgag.spongycastle:core:" + Versions.spongycastleCryptoCore - const val reKotlin = "org.rekotlin:rekotlin:" + Versions.rekotlin - const val retrofit = "com.squareup.retrofit2:retrofit:" + Versions.retrofit - const val retrofitMoshiConverter = "com.squareup.retrofit2:converter-moshi:" + Versions.retrofitMoshiConverter - const val timber = "com.jakewharton.timber:timber:" + Versions.timber - const val viewBindingDelegate = - "com.github.kirich1409:viewbindingpropertydelegate-noreflection:" + Versions.viewBindingDelegate - const val xmlShimmer = "com.github.skydoves:androidveil:" + Versions.xmlShimmer - const val zendeskChat = "com.zendesk:chat:" + Versions.zendeskChat - const val zendeskMessaging = "com.zendesk:messaging:" + Versions.zendeskMessaging - const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner - const val zxingQrCore = "com.google.zxing:core:" + Versions.zxingQrCode - const val mviCoreWatcher = "com.github.badoo.mvicore:mvicore-diff:" + Versions.mviCore - const val kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:" + Versions.kotlinSerialization -} - -object Tangem { - const val blockchain = "com.tangem:blockchain:" + Versions.tangemBlockchainSdk - const val cardAndroid = "com.tangem.tangem-sdk-kotlin:android:" + Versions.tangemCardSdk - const val cardCore = "com.tangem.tangem-sdk-kotlin:core:" + Versions.tangemCardSdk -} - -object Tools { - const val composeDetektRules = "ru.kode:detekt-rules-compose:" + Versions.composeDetektRules - const val formattingDetektRules = "io.gitlab.arturbosch.detekt:detekt-formatting:" + Versions.formattingDetektRules -} - -object Test { - const val espresso = "androidx.test.espresso:espresso-core:" + Versions.espresso - const val junit = "junit:junit:" + Versions.junit - const val junitAndroidExt = "androidx.test.ext:junit:" + Versions.junitAndroidExt - const val truth = "com.google.truth:truth:" + Versions.truth -} \ No newline at end of file diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt deleted file mode 100644 index c59727ad73..0000000000 --- a/buildSrc/src/main/java/Versions.kt +++ /dev/null @@ -1,82 +0,0 @@ -object Versions { - // region Classpath - const val androidGradlePlugin = "7.1.3" - const val androidMavenGradlePlugin = "2.1" - const val googleFirebaseCrashlytics = "2.7.1" - const val googleServices = "4.3.10" - const val kotlin = "1.7.10" - // endregion Classpath - - // region AndroidX - const val androidxActivityCompose = "1.5.0" - const val androidxAppCompat = "1.5.1" - const val androidxBrowser = "1.4.0" - const val androidxConstraintLayout = "2.1.4" - const val androidxCore = "1.9.0" - const val androidxFragment = "1.5.3" - const val androidxLifecycle = "2.5.1" - // endregion AndroidX - - // region Compose - const val compose = "1.3.1" - const val composeFoundation = "1.3.1" - const val composeConstraint = "1.0.1" - // endregion Compose - - // region Other libraries - const val accompanist = "0.28.0" - const val amplitude = "2.36.1" - const val appsflyer = "6.5.1" - const val armadillo = "0.9.0" - const val coil = "2.1.0" - const val composeShimmer = "1.0.3" - const val coroutine = "1.5.2" - const val desugarJdkLibs = "1.1.5" - const val firebase = "26.0.0" - const val googleMaterialComponent = "1.6.1" - const val googlePlayCore = "1.10.3" - const val googlePlayCoreKtx = "1.8.1" - const val googlePlayServicesWallet = "19.1.0" - const val hilt = "2.44" - const val jodatime = "2.12.1" - const val kotsonGsonExt = "2.5.0" - const val lottie = "3.4.0" - const val moshi = "1.12.0" - const val okhttp = "4.9.3" - const val otaliastudiosCameraView = "2.6.3" - const val rekotlin = "1.0.4" - const val retrofit = "2.8.1" - const val retrofitMoshiConverter = "2.6.0" - const val shopifyBuySdk = "12.0.0" - const val spongycastleCryptoCore = "1.58.0.0" - const val timber = "4.7.1" - const val viewBindingDelegate = "1.5.6" - const val xmlShimmer = "1.1.3" - const val zendeskChat = "3.3.5" - const val zendeskMessaging = "5.2.4" - const val zxingQrBarcodeScanner = "1.9.8" - const val zxingQrCode = "3.3.3" - const val mviCore = "1.3.1" - const val kotlinSerialization = "1.4.1" - // endregion Other libraries - - // region Tangem - const val tangemBlockchainSdk = "develop-174" - // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds - - const val tangemCardSdk = "develop-200" - // const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds - // endregion Tangem - - // region Tools - const val composeDetektRules = "1.2.2" - const val formattingDetektRules = "1.21.0" - // endregion Tools - - // region Testing - const val espresso = "3.4.0" - const val junit = "4.13.2" - const val truth = "1.1.3" - const val junitAndroidExt = "1.1.3" - // endregion Testing -} \ No newline at end of file diff --git a/buildSrc/src/main/java/detekt-convention.gradle.kts b/buildSrc/src/main/java/detekt-convention.gradle.kts deleted file mode 100644 index c78ee08e8e..0000000000 --- a/buildSrc/src/main/java/detekt-convention.gradle.kts +++ /dev/null @@ -1,33 +0,0 @@ -import io.gitlab.arturbosch.detekt.Detekt - -plugins { - id("io.gitlab.arturbosch.detekt") -} - -detekt { - parallel = true - ignoreFailures = false - autoCorrect = true - buildUponDefaultConfig = true - config.setFrom(rootProject.files("config/detekt/detekt.yml")) -} - -tasks.withType { - include("**/*.kt") - exclude("**/resources/**", "**/build/**") - reports { - sarif { - required.set(false) - } - txt { - required.set(true) - } - } - - jvmTarget = "1.8" -} - -dependencies { - detektPlugins(Tools.composeDetektRules) - detektPlugins(Tools.formattingDetektRules) -} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index e8259cee84..7ff7fb7522 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,9 +1,4 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + alias(deps.plugins.kotlin.jvm) + id("configuration") } \ No newline at end of file diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 639fbbc622..e1fb463a41 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -1,47 +1,16 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) /** Core shouldn't depends on core, but in case with utils and logging its necessary */ implementation(project(":core:utils")) diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4cd3a19a15..36c8fda7a2 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -1,40 +1,9 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { @@ -44,30 +13,31 @@ dependencies { implementation(project(":libs:auth")) /** Tangem libraries */ - implementation(Tangem.blockchain) - implementation(Tangem.cardCore) + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) /** Coroutines */ - implementation(Library.coroutine) + implementation(deps.kotlin.coroutines) /** Logging */ - implementation(Library.timber) + implementation(deps.timber) /** Network */ - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) - implementation(Library.moshi) - implementation(Library.moshiKotlin) - implementation(Library.okHttp) - implementation(Library.okHttpLogging) + implementation(deps.krateSharedPref) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.okHttp) + implementation(deps.okHttp.logging) + implementation(deps.retrofit) + implementation(deps.retrofit.moshi) /** Time */ - implementation(Library.jodatime) + implementation(deps.jodatime) /** Security */ - implementation(Library.spongecastleCryptoCore) + implementation(deps.spongecastle.core) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt new file mode 100644 index 0000000000..7e33fefae3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.asset + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.BufferedReader +import javax.inject.Inject + +/** + * Implementation of asset file reader + * + * @property context application context + */ +internal class AndroidAssetReader @Inject constructor( + @ApplicationContext private val context: Context, +) : AssetReader { + + override fun readJson(fileName: String): String { + return context.assets + .open("$fileName.json") + .bufferedReader() + .use(BufferedReader::readText) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt new file mode 100644 index 0000000000..6475ad3909 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.asset + +/** + * Asset file reader + * +[REDACTED_AUTHOR] + */ +interface AssetReader { + + /** Read content of json file [fileName] from asset */ + fun readJson(fileName: String): String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index d0c6d148c2..ed73d0bca4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -102,6 +102,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { saltPayAuthToken = configValues.saltPay.credentials.basicAuthToken, nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey), getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey), + kaspaSecondaryApiUrl = configValues.kaspaSecondaryApiUrl, ), appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt index 8ae508cbd3..829b539552 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt @@ -2,10 +2,10 @@ package com.tangem.datasource.config import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.models.ConfigModel import com.tangem.datasource.config.models.ConfigValueModel import com.tangem.datasource.config.models.FeatureModel -import com.tangem.datasource.utils.AssetReader import timber.log.Timber /** @@ -25,8 +25,8 @@ class FeaturesLocalLoader( val featureAdapter: JsonAdapter = moshi.adapter(FeatureModel::class.java) val valuesAdapter: JsonAdapter = moshi.adapter(ConfigValueModel::class.java) - val jsonFeatures = assetReader.readAssetAsString(featuresName) - val jsonConfigValues = assetReader.readAssetAsString(configValuesName) + val jsonFeatures = assetReader.readJson(featuresName) + val jsonConfigValues = assetReader.readJson(configValuesName) ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues)) } catch (ex: Exception) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 7bfabdebf1..ff996033fd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -35,6 +35,7 @@ class ConfigValueModel( val tronGridApiKey: String, val amplitudeApiKey: String, val swapReferrerAccount: SwapReferrerAccount?, + val kaspaSecondaryApiUrl: String, ) data class AppsFlyer( diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt index d5bff89d3b..695dbce8bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt @@ -15,7 +15,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,9 +35,9 @@ internal class AndroidNetworkConnectionManager @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : NetworkConnectionManager { - override val connectionStatus: StateFlow get() = _connectionStatus + override val isOnline: Boolean get() = _isOnline.value - private val _connectionStatus = MutableStateFlow(ConnectionStatus.OFFLINE) + private val _isOnline = MutableStateFlow(value = false) private val callbacks = NetworkConnectionManagerCallbacks() private val receiver = NetworkConnectionBroadcastReceiver() @@ -78,28 +77,26 @@ internal class AndroidNetworkConnectionManager @Inject constructor( CoroutineScope(SupervisorJob()).launch(dispatchers.io) { try { - val status = getCurrentStatus() + val status = isConnected() - _connectionStatus.emit(value = status) - Timber.i("Status changed to $status") + _isOnline.emit(value = status) + Timber.i("Connection status is ${if (status) "ONLINE" else "OFFLINE"}") } finally { pendingResult.finish() } } } - private fun getCurrentStatus(): ConnectionStatus { - connectivityManager ?: return ConnectionStatus.OFFLINE + private fun isConnected(): Boolean { + connectivityManager ?: return false - val isConnected = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) capabilities != null && capabilities.hasNetworkTransport() } else { val networkInfo = connectivityManager.activeNetworkInfo networkInfo != null && networkInfo.isConnectedOrConnecting } - - return if (isConnected) ConnectionStatus.ONLINE else ConnectionStatus.OFFLINE } private fun NetworkCapabilities.hasNetworkTransport(): Boolean { diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt deleted file mode 100644 index b59aa6f4df..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.datasource.connection - -/** - * Network connection status - * -[REDACTED_AUTHOR] - */ -enum class ConnectionStatus { - ONLINE, OFFLINE -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt index 9d6c73849d..fa6faca57d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt @@ -1,10 +1,8 @@ package com.tangem.datasource.connection -import kotlinx.coroutines.flow.StateFlow - /** Network connection manager */ interface NetworkConnectionManager { /** Connection status */ - val connectionStatus: StateFlow + val isOnline: Boolean } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt new file mode 100644 index 0000000000..2eefec13ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.AppPreferenceStorage +import com.tangem.datasource.local.AppPreferenceStorageImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AppPreferenceStorageModule { + + @Binds + @Singleton + fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt new file mode 100644 index 0000000000..7df5502310 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.asset.AndroidAssetReader +import com.tangem.datasource.asset.AssetReader +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AssetModule { + + @Binds + @Singleton + fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt new file mode 100644 index 0000000000..3e30c7344b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local + +/** + * Application local storage + * +[REDACTED_AUTHOR] + */ +interface AppPreferenceStorage { + + /** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */ + var featureToggles: String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt new file mode 100644 index 0000000000..3ce9a8aca9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.local + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import hu.autsoft.krate.SimpleKrate +import hu.autsoft.krate.default.withDefault +import hu.autsoft.krate.stringPref +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of application local storage + * + * @param context application context + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class AppPreferenceStorageImpl @Inject constructor( + @ApplicationContext context: Context, +) : SimpleKrate(context = context), AppPreferenceStorage { + + override var featureToggles: String by stringPref().withDefault("") +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt deleted file mode 100644 index 919d273aea..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.datasource.utils - -import android.content.Context - -/** -[REDACTED_AUTHOR] - */ -interface AssetReader { - fun readAssetAsString(name: String): String -} - -class AndroidAssetReader( - private val context: Context, -) : AssetReader { - - override fun readAssetAsString(name: String): String { - return context.readAssetAsString(name) - } -} - -fun Context.readAssetAsString(fileName: String): String { - return this.assets.open("$fileName.json").bufferedReader().readText() -} \ No newline at end of file diff --git a/core/featuretoggles/.gitignore b/core/featuretoggles/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/featuretoggles/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts new file mode 100644 index 0000000000..c5f9b07545 --- /dev/null +++ b/core/featuretoggles/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +dependencies { + implementation(project(":core:datasource")) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/AndroidManifest.xml b/core/featuretoggles/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f96c64f1db --- /dev/null +++ b/core/featuretoggles/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json new file mode 100644 index 0000000000..74e4d91915 --- /dev/null +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -0,0 +1,6 @@ +[ + { + "name": "OPTIMISM_SWAP_FEATURE_ENABLED", + "version": "4.3.1" + } +] \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt new file mode 100644 index 0000000000..b506d80ec4 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt @@ -0,0 +1,78 @@ +package com.tangem.core.featuretoggle.contract + +import androidx.annotation.VisibleForTesting +import timber.log.Timber + +/** + * Presentation of application version (..). + * + * @param value version value as string + * +[REDACTED_AUTHOR] + */ +internal class Version private constructor(value: String) : Comparable { + + private val major: Int + private val minor: Int + private val fix: Int? + + init { + val versions = value.split(VERSION_DELIMITER).map(String::toInt) + + major = versions.getVersionValue(index = MAJOR_VERSION_POSITION) + minor = versions.getVersionValue(index = MINOR_VERSION_POSITION) + fix = versions.getOrNull(index = FIX_VERSION_POSITION) + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + constructor(major: Int, minor: Int, fix: Int? = null) : this( + value = "$major.$minor${if (fix != null) ".$fix" else ""}", + ) + + override fun compareTo(other: Version): Int { + var result = major.compareTo(other.major) + if (result == 0) result = minor.compareTo(other.minor) + if (result == 0) { + when { + fix == null && other.fix == null -> result = 0 + fix == null && other.fix != null -> result = -1 + fix != null && other.fix == null -> result = 1 + fix != null && other.fix != null -> result = fix.compareTo(other.fix) + } + } + return result + } + + private fun List.getVersionValue(index: Int): Int { + return getOrNull(index) ?: error("Invalid version") + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getMajorVersion() = major + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getMinorVersion() = minor + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getFixVersion() = fix + + companion object { + private const val MAJOR_VERSION_POSITION = 0 + private const val MINOR_VERSION_POSITION = 1 + private const val FIX_VERSION_POSITION = 2 + private const val VERSION_DELIMITER = "." + + /** + * Create instance with value [value]. + * If [value] doesn't meet all requirements, the function returns null. + */ + fun create(value: String): Version? { + return try { + Version(value) + } catch (exception: Exception) { + Timber.e(exception, "Invalid version - %s", value) + return null + } + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt new file mode 100644 index 0000000000..2ee5c72e14 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt @@ -0,0 +1,20 @@ +package com.tangem.core.featuretoggle.contract + +/** + * Version contract to evaluate availability of feature toggle + * +[REDACTED_AUTHOR] + */ +internal object VersionAvailabilityContract { + + private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined" + + /** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */ + operator fun invoke(currentVersion: String, localVersion: String): Boolean { + if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false + val current = Version.create(currentVersion) ?: return false + val local = Version.create(localVersion) ?: return false + + return current >= local + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt new file mode 100644 index 0000000000..eadb4bf0fb --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt @@ -0,0 +1,55 @@ +package com.tangem.core.featuretoggle.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager +import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage +import com.tangem.core.featuretoggle.version.DefaultVersionProvider +import com.tangem.core.featuretoggles.BuildConfig +import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.local.AppPreferenceStorage +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureTogglesManagerModule { + + @Provides + @Singleton + @OptIn(ExperimentalStdlibApi::class) + fun provideFeatureTogglesManager( + @ApplicationContext context: Context, + assetReader: AssetReader, + appPreferenceStorage: AppPreferenceStorage, + ): FeatureTogglesManager { + val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + val localFeatureTogglesStorage = LocalFeatureTogglesStorage( + assetReader = assetReader, + jsonAdapter = moshi.adapter(), + ) + val versionProvider = DefaultVersionProvider(context) + + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + appPreferenceStorage = appPreferenceStorage, + jsonAdapter = moshi.adapter(), + versionProvider = versionProvider, + ) + } else { + ProdFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + versionProvider = versionProvider, + ) + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt new file mode 100644 index 0000000000..12e0a496b6 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -0,0 +1,59 @@ +package com.tangem.core.featuretoggle.manager + +import androidx.annotation.VisibleForTesting +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.datasource.local.AppPreferenceStorage +import kotlin.properties.Delegates + +/** + * Feature toggles manager implementation in DEV build + * + * @property localFeatureTogglesStorage local feature toggles storage + * @property appPreferenceStorage application local storage + * @property jsonAdapter adapter for parsing json + * @property versionProvider application version provider + */ +internal class DevFeatureTogglesManager( + private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val appPreferenceStorage: AppPreferenceStorage, + private val jsonAdapter: JsonAdapter>, + private val versionProvider: VersionProvider, +) : MutableFeatureTogglesManager { + + private var featureTogglesMap: MutableMap by Delegates.notNull() + + override suspend fun init() { + localFeatureTogglesStorage.init() + + val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) { + jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty() + } else { + emptyMap() + } + + featureTogglesMap = localFeatureTogglesStorage.featureToggles + .associateToggles(currentVersion = versionProvider.get().orEmpty()) + .mapValues { resultToggle -> + savedFeatureToggles[resultToggle.key] ?: resultToggle.value + } + .toMutableMap() + } + + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false + + override fun getFeatureToggles(): Map = featureTogglesMap + + override fun changeToggle(name: String, isEnabled: Boolean) { + featureTogglesMap[name] ?: return + featureTogglesMap[name] = isEnabled + appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap) + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun setFeatureToggles(map: MutableMap) { + featureTogglesMap = map + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt new file mode 100644 index 0000000000..6111e7f73b --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt @@ -0,0 +1,15 @@ +package com.tangem.core.featuretoggle.manager + +/** + * Component for getting information about the availability of feature toggles + * +[REDACTED_AUTHOR] + */ +interface FeatureTogglesManager { + + /** Initialize manager */ + suspend fun init() + + /** Check feature toggle availability by name [name] */ + fun isFeatureEnabled(name: String): Boolean +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt new file mode 100644 index 0000000000..abf6ff5b72 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt @@ -0,0 +1,15 @@ +package com.tangem.core.featuretoggle.manager + +/** + * Component for change information about the availability of feature toggles + * +[REDACTED_AUTHOR] + */ +interface MutableFeatureTogglesManager : FeatureTogglesManager { + + /** Get feature toggles */ + fun getFeatureToggles(): Map + + /** Change availability [isEnabled] of toggle with name [name] */ + fun changeToggle(name: String, isEnabled: Boolean) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt new file mode 100644 index 0000000000..0f88c3791c --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt @@ -0,0 +1,37 @@ +package com.tangem.core.featuretoggle.manager + +import androidx.annotation.VisibleForTesting +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import kotlin.properties.Delegates + +/** + * Feature toggles manager implementation in PROD build + * + * @property localFeatureTogglesStorage local feature toggles storage + * @property versionProvider application version provider + */ +internal class ProdFeatureTogglesManager( + private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val versionProvider: VersionProvider, +) : FeatureTogglesManager { + + private var featureToggles: Map by Delegates.notNull() + + override suspend fun init() { + localFeatureTogglesStorage.init() + featureToggles = localFeatureTogglesStorage.featureToggles + .associateToggles(currentVersion = versionProvider.get() ?: "") + } + + override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getProdFeatureToggles() = featureToggles + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun setProdFeatureToggles(map: Map) { + featureToggles = map + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt new file mode 100644 index 0000000000..69de7b76e7 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt @@ -0,0 +1,13 @@ +package com.tangem.core.featuretoggle.storage + +/** + * Data model with information about feature toggle + * + * @property name feature toggle name + * @property version version number in which the feature will be included + * + * IMPORTANT: if the version is "undefined", it means that feature toggle is disabled! + * +[REDACTED_AUTHOR] + */ +internal data class FeatureToggle(val name: String, val version: String) \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt new file mode 100644 index 0000000000..dd788efacb --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt @@ -0,0 +1,15 @@ +package com.tangem.core.featuretoggle.storage + +/** + * Component that initializes and stores a list of feature toggles + * +[REDACTED_AUTHOR] + */ +internal interface FeatureTogglesStorage { + + /** List of feature toggles */ + val featureToggles: List + + /** Initialize storage */ + suspend fun init() +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt new file mode 100644 index 0000000000..befc11c3b0 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt @@ -0,0 +1,42 @@ +package com.tangem.core.featuretoggle.storage + +import androidx.annotation.VisibleForTesting +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH +import com.tangem.datasource.asset.AssetReader +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Storage implementation for storing local feature toggles. + * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. + * + * @property assetReader asset reader + * @property jsonAdapter adapter for parsing local json config + * +[REDACTED_AUTHOR] + */ +internal class LocalFeatureTogglesStorage( + private val assetReader: AssetReader, + private val jsonAdapter: JsonAdapter>, +) : FeatureTogglesStorage { + + override var featureToggles: List by Delegates.notNull() + private set + + override suspend fun init() { + runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) } + .onSuccess { featureToggles = it } + .onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") } + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getConfigPath() = LOCAL_CONFIG_PATH + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getFeatureToggles(): Iterable = featureToggles + + private companion object { + const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt new file mode 100644 index 0000000000..990f95cf18 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt @@ -0,0 +1,13 @@ +package com.tangem.core.featuretoggle.utils + +import com.tangem.core.featuretoggle.contract.VersionAvailabilityContract +import com.tangem.core.featuretoggle.storage.FeatureToggle + +internal fun List.associateToggles(currentVersion: String): Map { + return associate { localToggle -> + Pair( + first = localToggle.name, + second = VersionAvailabilityContract(currentVersion, localToggle.version), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt new file mode 100644 index 0000000000..7cebaee8a8 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt @@ -0,0 +1,39 @@ +package com.tangem.core.featuretoggle.version + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * Implementation of application version provider + * + * @property context application context + */ +internal class DefaultVersionProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : VersionProvider { + + override fun get(): String? { + return runCatching { getVersionName().substringBefore(VERSION_NAME_DELIMITER) } + .fold(onSuccess = { it }, onFailure = { null }) + } + + private fun getVersionName(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager + .getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0), + ) + .versionName + } else { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } + } + + private companion object { + const val VERSION_NAME_DELIMITER = "-" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt new file mode 100644 index 0000000000..585caf85b8 --- /dev/null +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.core.featuretoggle.version + +/** Application version provider */ +internal interface VersionProvider { + + /** Get application version */ + fun get(): String? +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt new file mode 100644 index 0000000000..476c967367 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt @@ -0,0 +1,70 @@ +package com.tangem.core.featuretoggle.contract + +import com.google.common.truth.Truth +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class VersionAvailabilityContractTest { + + @Test + fun `local version is undefined`() { + val currentVersion = "0.0.0" + val localVersion = "undefined" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isFalse() + } + + @Test + fun `invalid current version`() { + val currentVersion = ".0.0" + val localVersion = "0.0.0" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isFalse() + } + + @Test + fun `invalid local version`() { + val currentVersion = "0.0.0" + val localVersion = ".0.0" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isFalse() + } + + @Test + fun `current version is greater than local version`() { + val currentVersion = "1.0.0" + val localVersion = "0.0.0" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isTrue() + } + + @Test + fun `current version is equal to local version`() { + val currentVersion = "0.0.1" + val localVersion = "0.0.1" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isTrue() + } + + @Test + fun `current version is less than local version`() { + val currentVersion = "0.0.0" + val localVersion = "0.1.0" + + val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion) + + Truth.assertThat(actual).isFalse() + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt new file mode 100644 index 0000000000..f8763c6a00 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt @@ -0,0 +1,121 @@ +package com.tangem.core.featuretoggle.contract + +import com.google.common.truth.Truth +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class VersionTest { + + @Test + fun `right versions order`() { + val major = 1 + val minor = 2 + val fix = 3 + val versionString = "$major.$minor.$fix" + + val version = Version.create(versionString) + + Truth.assertThat(version?.getMajorVersion()).isEqualTo(major) + Truth.assertThat(version?.getMinorVersion()).isEqualTo(minor) + Truth.assertThat(version?.getFixVersion()).isEqualTo(fix) + } + + @Test + fun `major version skipped`() { + Truth.assertThat(Version.create(".0.0")).isNull() + } + + @Test + fun `minor version skipped`() { + Truth.assertThat(Version.create("0..0")).isNull() + } + + @Test + fun `fix version skipped`() { + Truth.assertThat(Version.create("0.0.")).isNull() + } + + @Test + fun `optional fix version`() { + val actual = Version.create("0.0") + val expected = Version(major = 0, minor = 0) + + Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected) + } + + @Test + fun `full form version`() { + val actual = Version.create("0.0.0") + val expected = Version(major = 0, minor = 0, fix = 0) + + Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected) + } + + @Test + fun `compareTo if major version of one is greater than the other`() { + val one = Version.create("1.0.0") + val other = Version(major = 0, minor = 1, fix = 1) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER) + } + + @Test + fun `compareTo if minor version of one is greater than the other`() { + val one = Version.create("1.1.0") + val other = Version(major = 1, minor = 0, fix = 1) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER) + } + + @Test + fun `compareTo if fix version of one is greater than the other`() { + val one = Version.create("1.1.1") + val other = Version(major = 1, minor = 1, fix = 0) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER) + } + + @Test + fun `compareTo if one and other's fix version skipped`() { + val one = Version.create("1.1") + val other = Version(major = 1, minor = 1) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_EQUAL_TO_OTHER) + } + + @Test + fun `compareTo if one's fix version skipped`() { + val one = Version.create("1.1") + val other = Version(major = 1, minor = 1, fix = 0) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_LESS_THAN_OTHER) + } + + @Test + fun `compareTo if other's fix version skipped`() { + val one = Version.create("1.1.0") + val other = Version(major = 1, minor = 1) + + val actual = one?.compareTo(other) + + Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER) + } + + private companion object { + const val ONE_IS_GREATER_THAN_OTHER = 1 + const val ONE_IS_EQUAL_TO_OTHER = 0 + const val ONE_IS_LESS_THAN_OTHER = -1 + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..51ac267ee2 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt @@ -0,0 +1,242 @@ +package com.tangem.core.featuretoggle.manager + +import android.annotation.SuppressLint +import com.google.common.truth.Truth +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.storage.FeatureToggle +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.datasource.local.AppPreferenceStorage +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import io.mockk.verifyAll +import io.mockk.verifyOrder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SuppressLint("CheckResult") +internal class DevFeatureTogglesManagerTest { + + private val localFeatureTogglesStorage = mockk() + private val appPreferenceStorage = mockk(relaxed = true) + private val jsonAdapter = mockk>>() + private val versionProvider = mockk() + private val manager = DevFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + appPreferenceStorage = appPreferenceStorage, + jsonAdapter = jsonAdapter, + versionProvider = versionProvider, + ) + + @Test + fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest { + val currentVersion = "1.0.0" + + coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles + coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap + coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { versionProvider.get() } returns currentVersion + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + jsonAdapter.fromJson(savedFeatureToggles) + versionProvider.get() + } + + val expected = localFeatureToggles + .associateToggles(currentVersion) + .mapValues { resultToggle -> + savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value + } + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `successfully initialize storage if shared prefs kept empty list`() = runTest { + val currentVersion = "1.0.0" + val sharedPrefFeatureToggles = "[]" + + coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles + coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap + coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { versionProvider.get() } returns currentVersion + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + jsonAdapter.fromJson(sharedPrefFeatureToggles) + versionProvider.get() + } + + val expected = localFeatureToggles + .associateToggles(currentVersion) + .mapValues { resultToggle -> + savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value + } + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest { + val currentVersion = "1.0.0" + + coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { appPreferenceStorage.featureToggles } returns "" + coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { versionProvider.get() } returns currentVersion + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + versionProvider.get() + } + verifyAll(inverse = true) { jsonAdapter.fromJson(any()) } + + val expected = localFeatureToggles + .associateToggles(currentVersion) + .mapValues(Map.Entry::value) + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `successfully initialize storage if versionProvider returns null`() = runTest { + coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles + coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap + coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { versionProvider.get() } returns null + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + jsonAdapter.fromJson(savedFeatureToggles) + versionProvider.get() + } + + val expected = localFeatureToggles + .associateToggles(currentVersion = "") + .mapValues { resultToggle -> + savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value + } + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `get feature availability if feature toggle exists`() { + val featureToggles = mutableMapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to true, + "ACTIVE2_TEST_FEATURE_ENABLED" to true, + ) + manager.setFeatureToggles(featureToggles) + + val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED") + + Truth.assertThat(actual).isTrue() + } + + @Test + fun `get feature availability if feature toggle doesn't exists`() { + val featureToggles = mutableMapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to true, + ) + manager.setFeatureToggles(featureToggles) + + val actual = manager.isFeatureEnabled(name = "") + + Truth.assertThat(actual).isFalse() + } + + @Test + fun getFeatureToggles() { + val expected = mutableMapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + + manager.setFeatureToggles(expected) + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `change toggle that contains in map`() { + val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED" + val resultMap = mutableMapOf( + changeableToggleName to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + + manager.setFeatureToggles(resultMap) + coEvery { jsonAdapter.toJson(resultMap) } returns "" + + manager.changeToggle(changeableToggleName, true) + + resultMap[changeableToggleName] = true + verifyOrder { jsonAdapter.toJson(resultMap) } + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap) + } + + @Test + fun `change toggle that doesn't contains in map`() { + val resultMap = mutableMapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + + manager.setFeatureToggles(resultMap) + + manager.changeToggle("FEATURE_TOGGLE", true) + + verifyAll(inverse = true) { jsonAdapter.toJson(any()) } + + Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap) + } + + private companion object { + + val savedFeatureToggles = """ + [ + { + "name": "INACTIVE_TEST_FEATURE_ENABLED", + "version": "undefined" + }, + { + "name": "ACTIVE2_TEST_FEATURE_ENABLED", + "version": "1.0.0" + } + ] + """.trimIndent() + + val savedFeatureTogglesMap = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + + val localFeatureToggles = listOf( + FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..a8b90c4af8 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -0,0 +1,142 @@ +package com.tangem.core.featuretoggle.manager + +import android.content.pm.PackageManager +import com.google.common.truth.Truth +import com.tangem.core.featuretoggle.storage.FeatureToggle +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verifyAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class ProdFeatureTogglesManagerTest { + + private val localFeatureTogglesStorage = mockk() + private val versionProvider = mockk() + private val manager = ProdFeatureTogglesManager(localFeatureTogglesStorage, versionProvider) + + @Test + fun `successfully initialize storage`() = runTest { + val currentVersion = "1.0.0" + + coEvery { localFeatureTogglesStorage.init() } just Runs + every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + every { versionProvider.get() } returns currentVersion + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + versionProvider.get() + } + + val expected = localFeatureToggles.associateToggles(currentVersion) + Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected) + } + + @Test + fun `successfully initialize storage if versionProvider returns null`() = runTest { + coEvery { localFeatureTogglesStorage.init() } just Runs + every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + every { versionProvider.get() } returns null + + manager.init() + + coVerifyOrder { + localFeatureTogglesStorage.init() + versionProvider.get() + } + + Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles) + } + + @Test + fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest { + coEvery { localFeatureTogglesStorage.init() } just Runs + every { localFeatureTogglesStorage.featureToggles } throws IllegalStateException( + "Property featureToggles should be initialized before get.", + ) + + runCatching { manager.init() } + .onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") } + .onFailure { + Truth + .assertThat(it) + .hasMessageThat() + .contains("Property featureToggles should be initialized before get.") + + Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) + } + + coVerifyOrder { localFeatureTogglesStorage.init() } + verifyAll(inverse = true) { versionProvider.get() } + } + + @Test + fun `failure initialize storage if versionProvider throws exception`() = runTest { + coEvery { localFeatureTogglesStorage.init() } just Runs + every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + every { versionProvider.get() } throws PackageManager.NameNotFoundException() + + runCatching { manager.init() } + .onSuccess { throw IllegalStateException("versionProvider should throws exception") } + .onFailure { + Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java) + } + + coVerifyOrder { + localFeatureTogglesStorage.init() + versionProvider.get() + } + } + + @Test + fun `get feature availability if feature toggle exists`() { + val featureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to true, + "ACTIVE2_TEST_FEATURE_ENABLED" to true, + ) + manager.setProdFeatureToggles(featureToggles) + + val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED") + + Truth.assertThat(actual).isTrue() + } + + @Test + fun `get feature availability if feature toggle doesn't exists`() { + val featureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to true, + ) + manager.setProdFeatureToggles(featureToggles) + + val actual = manager.isFeatureEnabled(name = "") + + Truth.assertThat(actual).isFalse() + } + + private companion object { + val localFeatureToggles = listOf( + FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ) + + val disabledFeatureToggles = mapOf( + "INACTIVE_TEST_FEATURE_ENABLED" to false, + "ACTIVE2_TEST_FEATURE_ENABLED" to false, + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt new file mode 100644 index 0000000000..fae88c79f8 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt @@ -0,0 +1,107 @@ +package com.tangem.core.featuretoggle.storage + +import android.annotation.SuppressLint +import com.google.common.truth.Truth +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.asset.AssetReader +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.verifyAll +import io.mockk.verifyOrder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.io.IOException + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SuppressLint("CheckResult") +internal class LocalFeatureTogglesStorageTest { + + private val assetReader = mockk() + private val jsonAdapter = mockk>>() + private val storage = LocalFeatureTogglesStorage(assetReader, jsonAdapter) + + @Test + fun `successfully initialize storage`() = runTest { + coEvery { assetReader.readJson(storage.getConfigPath()) } returns json + coEvery { jsonAdapter.fromJson(json) } returns featureToggles + + storage.init() + + verifyOrder { + assetReader.readJson(storage.getConfigPath()) + jsonAdapter.fromJson(json) + } + + Truth.assertThat(storage.getFeatureToggles()).containsExactlyElementsIn(featureToggles) + } + + @Test + fun `failure initialize storage if assetReader throws exception`() = runTest { + coEvery { assetReader.readJson(storage.getConfigPath()) } returns json + coEvery { jsonAdapter.fromJson(json) } throws IOException() + + storage.init() + + verifyOrder { + assetReader.readJson(storage.getConfigPath()) + jsonAdapter.fromJson(json) + } + + runCatching { storage.getFeatureToggles() } + .onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") } + .onFailure { + Truth + .assertThat(it) + .hasMessageThat() + .contains("Property featureToggles should be initialized before get.") + + Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) + } + } + + @Test + fun `failure initialize storage if jsonAdapter throws exception`() = runTest { + coEvery { assetReader.readJson(storage.getConfigPath()) } throws IOException() + + storage.init() + + verifyOrder { assetReader.readJson(storage.getConfigPath()) } + verifyAll(inverse = true) { jsonAdapter.fromJson(any()) } + + runCatching { storage.getFeatureToggles() } + .onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") } + .onFailure { + Truth + .assertThat(it) + .hasMessageThat() + .contains("Property featureToggles should be initialized before get.") + + Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) + } + } + + private companion object { + + val json = """ + [ + { + "name": "INACTIVE_TEST_FEATURE_ENABLED", + "version": "undefined" + }, + { + "name": "ACTIVE2_TEST_FEATURE_ENABLED", + "version": "1.0.0" + } + ] + """.trimIndent() + + val featureToggles = listOf( + FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ) + } +} \ No newline at end of file diff --git a/core/res/build.gradle.kts b/core/res/build.gradle.kts index 77151f2a46..1fbece85d5 100644 --- a/core/res/build.gradle.kts +++ b/core/res/build.gradle.kts @@ -1,35 +1,5 @@ plugins { - id("com.android.library") - kotlin("android") -} - -android { - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index 272b4f483c..b8c91dd3d6 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -3,6 +3,8 @@ Default Legacy Erhalt der Gebühr fehlgeschlagen + Not enough funds for the transaction. Please top up your account. + Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen Destination account is not active. Send %s or more to activate the account. Interner Fehler der Blockchain diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 1bfd82f0a6..8a4f00445a 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -3,6 +3,8 @@ Default Legacy Échec de réception des commissions + Not enough funds for the transaction. Please top up your account. + Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. Pour créer un compte, téléchargez %1$s+ %2$s Destination account is not active. Send %s or more to activate the account. Erreur interne de la blockchain diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index e05f2a9a89..2581b20ac9 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -3,6 +3,8 @@ Default Legacy Impossibile ottenere la commissione + Not enough funds for the transaction. Please top up your account. + Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. Scarica %1$s+ %2$s per creare un account Destination account is not active. Send %s or more to activate the account. Errore interno della blockchain diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index 74528753b1..53b0aa27f3 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -3,6 +3,8 @@ По умолчанию Устаревший Не удалось получить комиссию + Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. + Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму. Пополните счет на %1$s+ %2$s, чтобы создать аккаунт Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Внутренняя ошибка блокчейна diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 064dc69503..d593a969ce 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -3,8 +3,10 @@ 默認 遺留資產 獲取費用失敗 + 沒有足夠的資金進行交易。請先入金 + Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. 加載 %1$s+ %2$s 以創建帳戶 - 目標帳戶未激活。發送 %s 或更多以激活帳戶。 + 目標帳戶未激活。發送 %s 或更多以激活帳戶 區塊鏈內部錯誤 最小數量是 %s 更動太小 diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index 3a01e0328f..c3df35291d 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -3,6 +3,8 @@ Default Legacy Failed to get fee + Not enough funds for the transaction. Please top up your account. + Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. Load %1$s+ %2$s to create account Destination account is not active. Send %s or more to activate the account. Blockchain internal error diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 28792f7aa2..20f1a9f4b7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,61 +1,22 @@ plugins { - id("com.android.library") - kotlin("android") -} - -android { - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Versions.compose - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") } dependencies { /** AndroidX libraries */ - implementation(AndroidX.fragmentKtx) + implementation(deps.androidx.fragment.ktx) /** Compose */ - implementation(Compose.foundation) - implementation(Compose.material) - implementation(Compose.uiTooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material) + implementation(deps.compose.ui.tooling) /** Other libraries */ - implementation(Library.accompanistSystemUiController) - implementation(Library.materialComponent) - implementation(Library.composeShimmer) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.material) + implementation(deps.compose.shimmer) implementation(project(":core:res")) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index ae4d3a5c93..f3d1e037fd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -32,6 +32,7 @@ fun getActiveIconRes(blockchainId: String): Int { "Kusama" -> R.drawable.img_kusama_22 "OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22 "DASH" -> R.drawable.img_dash_22 + "KAS" -> R.drawable.img_kaspa_22 else -> R.drawable.ic_alert_24 } } @@ -70,6 +71,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int { "ethereumfair" -> R.drawable.img_eth_fair_22 "kusama" -> R.drawable.img_kusama_22 "dash" -> R.drawable.img_dash_22 + "kaspa" -> R.drawable.img_kaspa_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_kaspa_22.xml b/core/ui/src/main/res/drawable/img_kaspa_22.xml new file mode 100644 index 0000000000..0fe4d6db53 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_kaspa_22.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 9f88778542..1e8960ce77 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -1,20 +1,15 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") - kotlin("kapt") + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + id("configuration") } dependencies { /** DI */ - implementation(Library.hiltCore) - kapt(Library.hiltKapt) + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) /** Coroutines */ - implementation(Library.coroutine) -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + implementation(deps.kotlin.coroutines) } \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 33ffebc8ed..6ea5884226 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,73 +1,7 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") -} - -android { - compileSdk = AppConfig.compileSdkVersion - - defaultConfig { - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("prod"), - Field.TestActionEnabled(false), - Field.LogEnabled(false), - ), - builder = ::buildConfigField, - ).create() - } - - debug { - isMinifyEnabled = false - - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("prod"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - packagingOptions { - resources.excludes += "lib/x86_64/darwin/libscrypt.dylib" - resources.excludes += "lib/x86_64/freebsd/libscrypt.so" - resources.excludes += "lib/x86_64/linux/libscrypt.so" - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") } dependencies { @@ -77,28 +11,28 @@ dependencies { implementation(project(":libs:auth")) /** Tangem libraries */ - implementation(Tangem.blockchain) { + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } - implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) { + implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { exclude(module = "joda-time") } /** Other libraries */ - implementation(Library.reKotlin) + implementation(deps.reKotlin) //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) - implementation(Library.moshi) - implementation(Library.moshiKotlin) - implementation(Library.okHttpLogging) - implementation(Library.timber) - implementation(Library.coroutine) + implementation(deps.retrofit) + implementation(deps.retrofit.moshi) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.okHttp.logging) + implementation(deps.timber) + implementation(deps.kotlin.coroutines) /** Testing libraries */ - testImplementation(Test.junit) - testImplementation(Test.truth) - androidTestImplementation(Test.junitAndroidExt) - androidTestImplementation(Test.espresso) + testImplementation(deps.test.junit) + testImplementation(deps.test.truth) + androidTestImplementation(deps.test.junit.android) + androidTestImplementation(deps.test.espresso) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt b/domain/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt new file mode 100644 index 0000000000..670e104425 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt @@ -0,0 +1,412 @@ +package com.tangem.domain.common.demo + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.features.BuildConfig +import java.math.BigDecimal + +@Suppress("LargeClass") +class DemoConfig { + + val demoBlockchains = listOf( + Blockchain.Bitcoin, + Blockchain.Ethereum, + Blockchain.Dogecoin, + Blockchain.Solana, + ) + + val demoCardIds: List by lazy { + val demoIds = getReleaseIds().toMutableList() + if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds) + + return@lazy demoIds.distinct() + } + + private val walletBalances: Map = mapOf( + Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin), + Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum), + Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin), + Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana), + ) + + fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId) + + fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId) + + fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy() + ?: Amount(BigDecimal.ZERO, blockchain).copy() + + private fun getReleaseIds(): List { + return (releaseDemoCardIds + testDemoCardIds).distinct() + } + + @Suppress("ClassOrdering") + private val releaseDemoCardIds = mutableListOf( + // === Not from the Google Sheet table === + "AC01000000041225", + "AC01000000041472", + "AB01000000046498", + "AB01000000049608", + "AB01000000049574", + "AB01000000046704", + "AB02000000051000", + "AB02000000050911", + + // === Mvideo === + // Wallet + "AC01000000045754", + "AC01000000041662", + "AC01000000041647", + "AC01000000041209", + "AC01000000042462", + "AC01000000041100", + "AC01000000041621", + "AC01000000045960", + "AC01000000041092", + "AC01000000041217", + "AC01000000013489", + "AC01000000028610", + "AC01000000028701", + "AC01000000028578", + "AC01000000027281", + "AC01000000027216", + "AC01000000028594", + "AC01000000028602", + "AC01000000028636", + "AC01000000013968", + "AC01000000027208", + "AC01000000013471", + "AC01000000028586", + "AC01000000013703", + "AC01000000028628", + "AC01000000028693", + "AC01000000028685", + "AC01000000013950", + "AC01000000013828", + "AC01000000013497", + "AC01000000013836", + "AC01000000013505", + "AC03000000046693", + "AC03000000046685", + "AC03000000046677", + "AC03000000046669", + "AC03000000046651", + "AC03000000046644", + "AC03000000046636", + "AC03000000046628", + "AC03000000046610", + "AC03000000046602", + "AC03000000046594", + "AC03000000046586", + "AC03000000046578", + "AC03000000046560", + "AC03000000046552", + "AC03000000046545", + "AC03000000046537", + "AC03000000046529", + "AC03000000046511", + "AC03000000046800", + "AC03000000046792", + "AC03000000046784", + "AC03000000046776", + "AC03000000046768", + "AC03000000046750", + "AC03000000046743", + "AC03000000046735", + "AC03000000046727", + "AC03000000046446", + "AC03000000046438", + "AC03000000046412", + "AC03000000046388", + "AC03000000046370", + "AC03000000046354", + "AC03000000046347", + "AC03000000046339", + "AC03000000046321", + "AC03000000046172", + "AC03000000046396", + "AC03000000046404", + "AC03000000046701", + "AC03000000046420", + "AC03000000046719", + "AC03000000046503", + "AC03000000046495", + "AC03000000046487", + "AC03000000046362", + "AC03000000046479", + "AC03000000046461", + "AC03000000046453", + + // Note BTC + "AB01000000059608", + "AB01000000046647", + "AB01000000046571", + "AB01000000046746", + "AB01000000059574", + "AB01000000046753", + "AB01000000046605", + "AB01000000046761", + "AB01000000046720", + "AB01000000046530", + "AB01000000016475", + "AB01000000016483", + "AB01000000016491", + "AB01000000020709", + "AB01000000020717", + "AB01000000015550", + "AB01000000015394", + "AB01000000016079", + "AB01000000016087", + "AB01000000016095", + "AB01000000020915", + "AB01000000017184", + "AB01000000020907", + "AB01000000017192", + "AB01000000016210", + "AB01000000016111", + "AB01000000016103", + "AB01000000015766", + "AB01000000015774", + "AB01000000015782", + "AB01000000022598", + "AB01000000022580", + "AB01000000005688", + "AB07000000005696", + "AB07000000005902", + "AB07000000005910", + "AB07000000005928", + "AB07000000005936", + "AB07000000005944", + "AB07000000005993", + "AB07000000005985", + "AB07000000005977", + "AB07000000005969", + "AB07000000005951", + "AB07000000005605", + "AB07000000005803", + "AB07000000005811", + "AB07000000005829", + "AB07000000005837", + "AB07000000005845", + "AB07000000005852", + "AB07000000005860", + "AB07000000005878", + "AB07000000005886", + "AB07000000005894", + "AB07000000005704", + "AB07000000005712", + "AB07000000005720", + "AB07000000005738", + "AB07000000005746", + "AB07000000005514", + "AB07000000005522", + "AB07000000005563", + "AB07000000005571", + "AB07000000005589", + "AB07000000005597", + "AB07000000005613", + "AB07000000005621", + "AB07000000005639", + "AB07000000005647", + "AB07000000005654", + "AB07000000005662", + "AB07000000005670", + "AB07000000005530", + "AB07000000005548", + "AB07000000005555", + "AB07000000005753", + "AB07000000005761", + "AB07000000005779", + "AB07000000005787", + "AB07000000005795", + "AB07000000005506", + + // Note ETH + "AB02000000051083", + "AB02000000051059", + "AB02000000051158", + "AB02000000050986", + "AB02000000051026", + "AB02000000050960", + "AB02000000051042", + "AB02000000051091", + "AB02000000051034", + "AB02000000051133", + "AB02000000019924", + "AB02000000019932", + "AB02000000022092", + "AB02000000022282", + "AB02000000023983", + "AB02000000023439", + "AB02000000020328", + "AB02000000020310", + "AB02000000021565", + "AB02000000022357", + "AB02000000023355", + "AB02000000022324", + "AB02000000022100", + "AB02000000019999", + "AB02000000020013", + "AB02000000020005", + "AB02000000020021", + "AB02000000020039", + "AB02000000020278", + "AB02000000020252", + "AB02000000018652", + "AB02000000018561", + "AB08000000009481", + "AB08000000009473", + "AB08000000009705", + "AB08000000009897", + "AB08000000009689", + "AB08000000009671", + "AB08000000009465", + "AB08000000009457", + "AB08000000009440", + "AB08000000009432", + "AB08000000009424", + "AB08000000009416", + "AB08000000009408", + "AB08000000009390", + "AB08000000009374", + "AB08000000009382", + "AB08000000009267", + "AB08000000009275", + "AB08000000009283", + "AB08000000009291", + "AB08000000009309", + "AB08000000009317", + "AB08000000009325", + "AB08000000009333", + "AB08000000009341", + "AB08000000009358", + "AB08000000009366", + "AB08000000009077", + "AB08000000009143", + "AB08000000009168", + "AB08000000009184", + "AB08000000009192", + "AB08000000009200", + "AB08000000009226", + "AB08000000009218", + "AB08000000009234", + "AB08000000009242", + "AB08000000008574", + "AB08000000009069", + "AB08000000008525", + "AB08000000009051", + "AB08000000009135", + "AB08000000009150", + "AB08000000009176", + "AB08000000009085", + "AB08000000009093", + "AB08000000009101", + "AB08000000009119", + "AB08000000009127", + "AB08000000009259", + + // === Technopark === + // Wallet + "AC01000000044120", + "AC01000000044997", + "AC01000000044989", + "AC01000000043494", + "AC01000000043486", + "AC01000000044187", + "AC01000000043148", + "AC01000000044013", + "AC01000000043973", + "AC01000000044815", + "AC01000000044807", + "AC01000000043809", + "AC01000000043833", + "AC01000000043460", + "AC01000000043064", + "AC01000000044138", + "AC01000000044500", + "AC01000000044492", + "AC01000000044260", + "AC01000000044278", + + // Note BTC + "AB01000000049864", + "AB01000000053239", + "AB01000000053056", + "AB01000000054237", + "AB01000000054245", + "AB01000000054211", + "AB01000000054229", + "AB01000000053189", + "AB01000000054195", + "AB01000000050797", + "AB01000000053833", + "AB01000000052124", + "AB01000000051605", + "AB01000000052223", + "AB01000000052207", + "AB01000000052199", + "AB01000000047785", + "AB01000000047850", + "AB01000000047868", + "AB01000000048288", + + // Note ETH + "AB02000000049715", + "AB02000000049848", + "AB02000000049814", + "AB02000000049863", + "AB02000000049871", + "AB02000000049855", + "AB02000000049285", + "AB02000000049277", + "AB02000000049558", + "AB02000000049889", + "AB02000000049988", + "AB02000000049707", + "AB02000000049699", + "AB02000000049897", + "AB02000000049905", + "AB02000000049913", + "AB02000000049251", + "AB02000000049533", + "AB02000000049541", + "AB02000000049830", + // === more cids === + "AC03000000091418", + "AC03000000091400", + "AC03000000099007", + "AC03000000098991", + "AC03000000098942", + "AC03000000091715", + "AC03000000091301", + "AC03000000091343", + "AB01000000055705", + "AB01000000052918", + "AB01000000047710", + "AB01000000052306", + "AB01000000047645", + "AB01000000048957", + "AB01000000052900", + "AB01000000050391", + "AB01000000047363", + "AB02000000053998", + "AB02000000019809", + "AB02000000020872", + "AB02000000022027", + "AB02000000058955", + "AB02000000053253", + "AB02000000048063", + "AB02000000023736", + "AB02000000058187", + ) + + @Suppress("ClassOrdering") + private val testDemoCardIds = listOf( + "FB20000000000186", // Note ETH + "FB10000000000196", // Note BTC + "FB30000000000176", // Wallet + ) + + @Suppress("ClassOrdering") + private val debugTestDemoCardIds = listOf() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index f8bb58f3f1..9ec8533263 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -49,6 +49,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "optimistic-ethereum/test" -> Blockchain.OptimismTestnet "dash" -> Blockchain.Dash "sxdai" -> Blockchain.SaltPay + "kaspa" -> Blockchain.Kaspa else -> null } } @@ -102,6 +103,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.OptimismTestnet -> "optimistic-ethereum/test" Blockchain.Dash -> "dash" Blockchain.SaltPay -> "sxdai" + Blockchain.Kaspa -> "kaspa" } } @@ -135,6 +137,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Optimism, Blockchain.OptimismTestnet -> "ethereum" Blockchain.Dash -> "dash" Blockchain.SaltPay -> "xdai" + Blockchain.Kaspa -> "kaspa" Blockchain.Unknown -> "unknown" } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d93ec16fc3..d1867f7c33 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -20,7 +20,7 @@ platform :android do before_all do |lane, options| FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services.json", "../app") end - + desc "Runs all the tests" lane :test do gradle( @@ -44,11 +44,11 @@ platform :android do }) end - desc "Build debug and release APKs" + desc "Build internal and release APKs" lane :build do |options| gradle( task: "clean assemble", - build_type: "Debug_beta", + build_type: "Internal", properties: { 'versionCode' => options[:versionCode], 'versionName' => options[:versionName], diff --git a/fastlane/README.md b/fastlane/README.md index 4d1e079727..9df376cd1d 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -37,7 +37,7 @@ Build a signed release APK [bundle exec] fastlane android build ``` -Build debug and release APKs +Build internal and release APKs ### android beta diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 510d84817c..ab23b2ba7a 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -1,41 +1,9 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { @@ -46,9 +14,9 @@ dependencies { implementation(project(":features:referral:domain")) /** Time */ - implementation(Library.jodatime) + implementation(deps.jodatime) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index fe5dace957..6f335c1221 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") - kotlin("kapt") + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + id("configuration") } dependencies { @@ -11,14 +11,9 @@ dependencies { implementation(project(":libs:crypto")) /** Time */ - implementation(Library.jodatime) + implementation(deps.jodatime) /** DI */ - implementation(Library.hiltCore) - kapt(Library.hiltKapt) -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 7096e631c0..1f66fc2708 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -1,49 +1,9 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Versions.compose - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { @@ -52,26 +12,27 @@ dependencies { implementation(project(":core:res")) implementation(project(":core:utils")) implementation(project(":core:ui")) + implementation(project(":libs:crypto")) /** AndroidX */ - implementation(AndroidX.appCompat) - implementation(AndroidX.fragmentKtx) - implementation(AndroidX.lifecycleViewModelKtx) - implementation(Library.materialComponent) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.fragment.ktx) + implementation(deps.lifecycle.viewModel.ktx) + implementation(deps.material) /** Compose */ - implementation(Compose.foundation) - implementation(Compose.material) - implementation(Compose.uiTooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material) + implementation(deps.compose.ui.tooling) /** Domain */ implementation(project(":features:referral:domain")) /** Other libraries */ - implementation(Library.composeShimmer) - implementation(Library.accompanistWebView) + implementation(deps.compose.shimmer) + implementation(deps.compose.accompanist.webView) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index 2432d8c1e0..8b1bf84d3d 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -15,6 +15,7 @@ import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState import com.tangem.feature.referral.router.ReferralRouter +import com.tangem.lib.crypto.models.errors.UserCancelledException import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel @@ -34,6 +35,8 @@ internal class ReferralViewModel @Inject constructor( private var referralRouter: ReferralRouter by Delegates.notNull() + private val lastReferralData = mutableStateOf(null) + init { loadReferralData() } @@ -61,7 +64,11 @@ internal class ReferralViewModel @Inject constructor( private fun loadReferralData() { uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) viewModelScope.launch(dispatchers.main) { - runCatching(dispatchers.io) { referralInteractor.getReferralStatus() } + runCatching(dispatchers.io) { + referralInteractor.getReferralStatus().apply { + lastReferralData.value = this + } + } .onSuccess(::showContent) .onFailure(::showErrorSnackbar) } @@ -73,7 +80,16 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.startReferral() } .onSuccess(::showContent) - .onFailure(::showErrorSnackbar) + .onFailure { + if (it is UserCancelledException) { + val lastRefData = lastReferralData.value + if (lastRefData != null) { + showContent(lastRefData) + } + } else { + showErrorSnackbar(it) + } + } } } diff --git a/features/swap/api/.gitignore b/features/swap/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/api/build.gradle.kts b/features/swap/api/build.gradle.kts new file mode 100644 index 0000000000..882b05bd82 --- /dev/null +++ b/features/swap/api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +dependencies { + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/swap/api/src/main/AndroidManifest.xml b/features/swap/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b46e5ddb0b --- /dev/null +++ b/features/swap/api/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt b/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt new file mode 100644 index 0000000000..c9b4962de4 --- /dev/null +++ b/features/swap/api/src/main/java/com/tangem/feature/swap/api/SwapFeatureToggleManager.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.swap.api + +/** Feature toggles manager of "swap" feature */ +interface SwapFeatureToggleManager { + + val isOptimismSwapEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 9f2c7c0c4d..87bb14fe6b 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -1,41 +1,9 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { @@ -46,9 +14,9 @@ dependencies { implementation(project(":features:swap:domain")) /** Network */ - implementation(Library.retrofit) + implementation(deps.retrofit) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 87fa664521..d5dec7c6d0 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -1,8 +1,8 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") - kotlin("kapt") - kotlin("plugin.serialization") + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + id("configuration") } dependencies { @@ -11,14 +11,9 @@ dependencies { implementation(project(":core:utils")) /** DI */ - implementation(Library.hiltCore) - kapt(Library.hiltKapt) + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) /** Other Libraries **/ - implementation(Library.kotlinSerialization) -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 287b9e6ad6..873966b94c 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -1,82 +1,45 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") - id("com.google.dagger.hilt.android") -} - -android { - - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } - - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Versions.compose - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(project(":core:featuretoggles")) implementation(project(":core:utils")) implementation(project(":core:ui")) /** AndroidX */ - implementation(AndroidX.activityCompose) - implementation(AndroidX.appCompat) - implementation(AndroidX.fragmentKtx) - implementation(AndroidX.lifecycleViewModelKtx) - implementation(AndroidX.browser) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.fragment.ktx) + implementation(deps.lifecycle.viewModel.ktx) + implementation(deps.androidx.browser) /** Compose */ - implementation(Compose.foundation) - implementation(Compose.material) - implementation(Compose.uiTooling) - implementation(Compose.coil) - implementation(Compose.constraintLayout) + implementation(deps.compose.foundation) + implementation(deps.compose.material) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.compose.constraintLayout) + + /** Api */ + implementation(project(":features:swap:api")) /** Domain */ implementation(project(":features:swap:domain")) - implementation(project(":features:swap:data"))//todo remove, only for test /** Other libraries */ - implementation(Library.composeShimmer) - implementation(Library.kotlinSerialization) - implementation(Library.timber) + implementation(deps.compose.shimmer) + implementation(deps.kotlin.serialization) + implementation(deps.timber) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt new file mode 100644 index 0000000000..15ef8179e1 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapFeatureTogglesManagerModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.swap.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.feature.swap.api.SwapFeatureToggleManager +import com.tangem.feature.swap.toggles.DefaultFeatureTogglesManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@Module +@InstallIn(ActivityRetainedComponent::class) +internal object SwapFeatureTogglesManagerModule { + + @Provides + @ActivityRetainedScoped + fun provideSwapFeatureTogglesManager(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggleManager { + return DefaultFeatureTogglesManager(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt new file mode 100644 index 0000000000..2448fefafd --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/DefaultFeatureTogglesManager.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.swap.toggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +/** Feature toggles manager implementation of "swap" feature */ +class DefaultFeatureTogglesManager( + private val featureTogglesManager: FeatureTogglesManager, +) : InnerSwapFeatureTogglesManager { + + override val isOptimismSwapEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "OPTIMISM_SWAP_FEATURE_ENABLED") +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt new file mode 100644 index 0000000000..f1035d737a --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/toggles/InnerSwapFeatureTogglesManager.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.swap.toggles + +import com.tangem.feature.swap.api.SwapFeatureToggleManager + +/** Feature toggles manager of "swap" feature for internal logic */ +internal interface InnerSwapFeatureTogglesManager : SwapFeatureToggleManager \ No newline at end of file diff --git a/features/tester/api/.gitignore b/features/tester/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/tester/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/tester/api/build.gradle.kts b/features/tester/api/build.gradle.kts new file mode 100644 index 0000000000..1fbece85d5 --- /dev/null +++ b/features/tester/api/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} \ No newline at end of file diff --git a/features/tester/api/src/main/AndroidManifest.xml b/features/tester/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..99ac562598 --- /dev/null +++ b/features/tester/api/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt new file mode 100644 index 0000000000..913c04a076 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.tester.api + +/** + * Outer tester feature router + * +[REDACTED_AUTHOR] + */ +interface TesterRouter { + + /** Open tester menu */ + fun startTesterScreen() +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 8466265b89..774c318817 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -1,65 +1,32 @@ plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") -} - -android { - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - vectorDrawables { - useSupportLibrary = true - } - } - - kotlinOptions { - jvmTarget = "1.8" - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } - - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Versions.compose - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") } dependencies { /** AndroidX */ - implementation(AndroidX.activityCompose) + implementation(deps.androidx.activity.compose) /** Compose */ - implementation(Compose.material) - implementation(Compose.ui) - implementation(Compose.uiTooling) + implementation(deps.compose.material) + implementation(deps.compose.foundation) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) /** Core modules */ + implementation(project(":core:featuretoggles")) implementation(project(":core:ui")) + + /** Feature Apis */ + implementation(project(":features:tester:api")) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt new file mode 100644 index 0000000000..433341f7f7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.tester.di + +import com.tangem.feature.tester.presentation.navigation.DefaultTesterRouter +import com.tangem.features.tester.api.TesterRouter +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal interface TesterRouterModule { + + @Binds + @ActivityScoped + fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): TesterRouter +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index fb1f0e1777..f90be893ca 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -3,15 +3,73 @@ package com.tangem.feature.tester.presentation import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen +import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel +import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState +import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** Activity for testers */ @AndroidEntryPoint internal class TesterActivity : ComponentActivity() { + + /** Router for inner feature navigation */ + @Inject + lateinit var testerRouter: TesterRouter + + private val innerTesterRouter: InnerTesterRouter + get() = requireNotNull(testerRouter as? InnerTesterRouter) { + "TesterRouter must be InnerTesterRouter for tester feature" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { - // TODO("2949") add logic to screens + TangemTheme { + val statusBarColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setStatusBarColor(color = statusBarColor) + } + + TesterNavHost() + } + } + } + + @Suppress("TopLevelComposableFunctions") + @Composable + private fun TesterNavHost() { + val navController = rememberNavController().also { innerTesterRouter.setNavController(it) } + + NavHost(navController = navController, startDestination = TesterScreen.MENU.name) { + composable(route = TesterScreen.MENU.name) { + TesterMenuScreen( + state = TesterMenuContentState( + onBackClick = innerTesterRouter::back, + onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) }, + ), + ) + } + + composable(route = TesterScreen.FEATURE_TOGGLES.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + + FeatureTogglesScreen(state = viewModel.uiState) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt index b898f31db6..782c2a7714 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt @@ -8,7 +8,7 @@ package com.tangem.feature.tester.presentation.featuretoggles.models * [REDACTED_AUTHOR] */ -data class TesterFeatureToggle( +internal data class TesterFeatureToggle( val name: String, - val isEnabled: Boolean + val isEnabled: Boolean, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt new file mode 100644 index 0000000000..4c55258e0d --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.tester.presentation.featuretoggles.state + +import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle + +/** + * Content state of feature toggles screen + * + * @property featureToggles feature toggles list + * @property onBackClick the lambda to be invoked when back button is pressed + * @property onToggleValueChange the lambda to be invoked when switch button is pressed + */ +internal data class FeatureTogglesContentState( + val featureToggles: List, + val onBackClick: () -> Unit, + val onToggleValueChange: (String, Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt deleted file mode 100644 index 5dea9cf196..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.feature.tester.presentation.featuretoggles.state - -import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle - -/** - * State holder of feature toggles screen - * -[REDACTED_AUTHOR] - */ -sealed interface FeatureTogglesStateHolder { - - /** - * Content - * - * @property featureToggles feature toggles list - * @property onToggleValueChange the lambda to be invoked when switch button is pressed - * @property onBackClicked the lambda to be invoked when back button is pressed - */ - data class Content( - val onBackClicked: () -> Unit, - val featureToggles: List, - val onToggleValueChange: (String, Boolean) -> Unit, - ) : FeatureTogglesStateHolder -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 6d9680804e..c64548832a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -21,23 +21,16 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle -import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesStateHolder +import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState /** * Screen with feature toggles list * - * @param stateHolder screen state + * @param state screen state */ -@Composable -fun FeatureTogglesScreen(stateHolder: FeatureTogglesStateHolder) { - when (stateHolder) { - is FeatureTogglesStateHolder.Content -> FeatureTogglesContent(content = stateHolder) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable -private fun FeatureTogglesContent(content: FeatureTogglesStateHolder.Content) { +internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { LazyColumn( modifier = Modifier .fillMaxSize() @@ -45,14 +38,14 @@ private fun FeatureTogglesContent(content: FeatureTogglesStateHolder.Content) { ) { stickyHeader { AppBarWithBackButton( - onBackClick = content.onBackClicked, + onBackClick = state.onBackClick, text = stringResource(id = R.string.feature_toggles), ) } - items(content.featureToggles) { featureToggle -> + items(state.featureToggles) { featureToggle -> FeatureToggleItem( toggle = featureToggle, - onCheckedChange = { isChange -> content.onToggleValueChange(featureToggle.name, isChange) }, + onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) }, ) } } @@ -89,13 +82,13 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo private fun PreviewFeatureTogglesScreen_InLightTheme() { TangemTheme(isDark = false) { FeatureTogglesScreen( - stateHolder = FeatureTogglesStateHolder.Content( + state = FeatureTogglesContentState( featureToggles = listOf( TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), ), onToggleValueChange = { _, _ -> }, - onBackClicked = {}, + onBackClick = {}, ), ) } @@ -106,13 +99,13 @@ private fun PreviewFeatureTogglesScreen_InLightTheme() { private fun PreviewFeatureTogglesScreen_InDarkTheme() { TangemTheme(isDark = true) { FeatureTogglesScreen( - stateHolder = FeatureTogglesStateHolder.Content( + state = FeatureTogglesContentState( featureToggles = listOf( TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), ), onToggleValueChange = { _, _ -> }, - onBackClicked = {}, + onBackClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt new file mode 100644 index 0000000000..4c10030524 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.tester.presentation.featuretoggles.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager +import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle +import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * ViewModel for screen with list of feature toggles + * + * @property featureTogglesManager manager for getting information about the availability of feature toggles + * +[REDACTED_AUTHOR] + */ +@HiltViewModel +internal class FeatureTogglesViewModel @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : ViewModel() { + + /** Current ui state */ + var uiState: FeatureTogglesContentState by mutableStateOf(initState()) + private set + + private val mutableFeatureTogglesManager: MutableFeatureTogglesManager + get() = requireNotNull(featureTogglesManager as? MutableFeatureTogglesManager) { + "Feature toggle manager must be mutable (debug build type)" + } + + /** Setup navigation state property by router [router] */ + fun setupNavigation(router: InnerTesterRouter) { + uiState = uiState.copy(onBackClick = router::back) + } + + private fun initState(): FeatureTogglesContentState { + return FeatureTogglesContentState( + featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), + onBackClick = {}, + onToggleValueChange = ::onToggleValueChange, + ) + } + + private fun onToggleValueChange(name: String, isEnabled: Boolean) { + mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled) + + uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles()) + } + + private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List { + return this + .getFeatureToggles() + .map { TesterFeatureToggle(it.key, it.value) } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt new file mode 100644 index 0000000000..b99bc61405 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tester.presentation.menu.state + +/** + * Content state of tester menu screen + * + * @property onBackClick the lambda to be invoked when back button is pressed + * @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed + */ +data class TesterMenuContentState( + val onBackClick: () -> Unit, + val onFeatureTogglesClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt deleted file mode 100644 index b2afffb27b..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.feature.tester.presentation.menu.state - -/** - * State holder of tester menu screen - * -[REDACTED_AUTHOR] - */ -sealed interface TesterMenuStateHolder { - - /** - * Content - * - * @property onBackClicked the lambda to be invoked when back button is pressed - * @property onFeatureTogglesClicked the lambda to be invoked when feature toggles button is pressed - */ - data class Content( - val onBackClicked: () -> Unit, - val onFeatureTogglesClicked: () -> Unit - ) : TesterMenuStateHolder -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index 08b73b4e26..ac0c7d15d4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -16,29 +16,22 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.impl.R -import com.tangem.feature.tester.presentation.menu.state.TesterMenuStateHolder +import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState /** * Screen with functionality for testers * - * @param stateHolder screen state + * @param state screen state */ @Composable -fun TesterMenuScreen(stateHolder: TesterMenuStateHolder) { - when (stateHolder) { - is TesterMenuStateHolder.Content -> TesterMenuContent(content = stateHolder) - } -} - -@Composable -private fun TesterMenuContent(content: TesterMenuStateHolder.Content) { +internal fun TesterMenuScreen(state: TesterMenuContentState) { Column( modifier = Modifier .fillMaxSize() .background(TangemTheme.colors.background.secondary), ) { AppBarWithBackButton( - onBackClick = content.onBackClicked, + onBackClick = state.onBackClick, text = stringResource(id = R.string.tester_menu), ) Column( @@ -52,12 +45,12 @@ private fun TesterMenuContent(content: TesterMenuStateHolder.Content) { ) { PrimaryButton( text = stringResource(R.string.feature_toggles), - onClick = content.onFeatureTogglesClicked, + onClick = state.onFeatureTogglesClick, modifier = Modifier.fillMaxWidth(), ) PrimaryButton( text = stringResource(R.string.stand_toggles), - onClick = content.onFeatureTogglesClicked, + onClick = state.onFeatureTogglesClick, modifier = Modifier.fillMaxWidth(), enabled = false, ) @@ -70,9 +63,9 @@ private fun TesterMenuContent(content: TesterMenuStateHolder.Content) { private fun PreviewTesterMenuScreen_InLightTheme() { TangemTheme(isDark = false) { TesterMenuScreen( - stateHolder = TesterMenuStateHolder.Content( - onBackClicked = {}, - onFeatureTogglesClicked = {}, + state = TesterMenuContentState( + onBackClick = {}, + onFeatureTogglesClick = {}, ), ) } @@ -83,9 +76,9 @@ private fun PreviewTesterMenuScreen_InLightTheme() { private fun PreviewTesterMenuScreen_InDarkTheme() { TangemTheme(isDark = true) { TesterMenuScreen( - stateHolder = TesterMenuStateHolder.Content( - onBackClicked = {}, - onFeatureTogglesClicked = {}, + state = TesterMenuContentState( + onBackClick = {}, + onFeatureTogglesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt new file mode 100644 index 0000000000..97126f3540 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.tester.presentation.navigation + +import android.content.Context +import android.content.Intent +import androidx.navigation.NavController +import com.tangem.feature.tester.presentation.TesterActivity +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped +import javax.inject.Inject + +/** + * Implementation of router for tester feature + * + * @property context activity context + * +[REDACTED_AUTHOR] + */ +@ActivityScoped +internal class DefaultTesterRouter @Inject constructor( + @ActivityContext private val context: Context, +) : InnerTesterRouter { + + private var navController: NavController? = null + + override fun startTesterScreen() { + context.startActivity( + Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + + override fun setNavController(navController: NavController) { + this.navController = navController + } + + override fun open(screen: TesterScreen) { + navController?.navigate(screen.name) + } + + override fun back() { + navController?.popBackStack() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt new file mode 100644 index 0000000000..0d5468767f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tester.presentation.navigation + +import androidx.navigation.NavController +import com.tangem.features.tester.api.TesterRouter + +/** + * Inner feature router + * +[REDACTED_AUTHOR] + */ +internal interface InnerTesterRouter : TesterRouter { + + /** Set up a navigation controller that bound to tester navigation graph */ + fun setNavController(navController: NavController) + + /** Open specified screen [screen] */ + fun open(screen: TesterScreen) + + /** Open last screen */ + fun back() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt new file mode 100644 index 0000000000..18934aa128 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tester.presentation.navigation + +/** + * Tester feature screens + * +[REDACTED_AUTHOR] + */ +internal enum class TesterScreen { + MENU, FEATURE_TOGGLES +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 52a563ad69..e3a622a5ef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,11 +6,12 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -org.gradle.parallel=true -org.gradle.daemon=true -android.useAndroidX=true -android.enableJetifier=true +org.gradle.parallel = true +org.gradle.daemon = true +android.useAndroidX = true +android.enableJetifier = true +org.gradle.unsafe.configuration-cache = true diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml new file mode 100644 index 0000000000..c673ea6135 --- /dev/null +++ b/gradle/dependencies.toml @@ -0,0 +1,203 @@ +# docs https://docs.gradle.org/current/userguide/platforms.html +# TODO: update versions refs https://tangem.atlassian.net/browse/AND-3195 + +[versions] +# region Classpath +androidGradlePlugin = "7.4.2" +firebaseCrashlytics = "2.9.4" +googleServices = "4.3.10" +kotlin = "1.8.10" +# endregion Classpath + +# region AndroidX +androidxActivityCompose = "1.5.0" +androidxAppCompat = "1.5.1" +androidxBrowser = "1.4.0" +androidxConstraintLayout = "2.1.4" +androidxCore = "1.9.0" +androidxFragment = "1.5.3" +androidxLifecycle = "2.5.1" +# endregion AndroidX + +# region Compose +compose-compiler = "1.4.3" +compose-runtime = "1.3.3" +compose-foundation = "1.3.1" +compose-material = "1.3.1" +compose-constraint = "1.0.1" +compose-navigation = "2.5.3" +compose-accompanist = "0.28.0" +# endregion Compose + +# region Other libraries +amplitude = "2.36.1" +appsflyer = "6.5.1" +armadillo = "0.9.0" +coil = "2.1.0" +compose-shimmer = "1.0.3" +coroutine = "1.5.2" +desugarJdkLibs = "1.1.5" +firebase = "26.0.0" +googleMaterialComponent = "1.6.1" +googlePlayCore = "1.10.3" +googlePlayCoreKtx = "1.8.1" +googlePlayServicesWallet = "19.1.0" +hilt = "2.44" +hilt-navigation = "1.0.0" +jodatime = "2.12.1" +krateSharedPref = "2.0.0" +kotsonGsonExt = "2.5.0" +lottie = "3.4.0" +moshi = "1.12.0" +okhttp = "4.9.3" +otaliastudiosCameraView = "2.6.3" +rekotlin = "1.0.4" +retrofit = "2.8.1" +retrofitMoshiConverter = "2.6.0" +shopifyBuySdk = "12.0.0" +spongycastleCryptoCore = "1.58.0.0" +timber = "4.7.1" +viewBindingDelegate = "1.5.6" +xmlShimmer = "1.1.3" +zendeskChat = "3.3.5" +zendeskMessaging = "5.2.4" +zxingQrBarcodeScanner = "1.9.8" +zxingQrCode = "3.3.3" +mviCore = "1.3.1" +kotlinSerialization = "1.4.1" +# endregion Other libraries + +# region Tangem +tangemBlockchainSdk = "develop-179" +#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds +tangemCardSdk = "develop-204" +# tangemCardSdk = "0.0.1" # Keep it! - used for local builds +# endregion Tangem + +# region Tools +detektComposeRules = "1.2.2" +detekt = "1.21.0" +# endregion Tools + +# region Testing +espresso = "3.4.0" +junit = "4.13.2" +junitAndroidExt = "1.1.3" +mockk = "1.13.4" +truth = "1.1.3" +# endregion Testing + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } + +[libraries] +# region Classpath +# Used for plugins +gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } +gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradle-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlytics" } +# end region Classpath + +# region AndroidX +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" } +androidx-appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } +androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" } +androidx-constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintLayout" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragment" } +lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } +lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } +# region AndroidX + +# region Compose +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" } +compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } +compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } +compose-constraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "compose-constraint" } +compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "compose-shimmer" } +compose-coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" } +compose-navigation-hilt = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation" } +compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "compose-accompanist" } +compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" } +compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" } +# endregion Compose + +# region Firebase +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +# endregion Firebase + +# region Tangem +tangem-blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } +tangem-card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } +tangem-card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +# endregion Tangem + +# region Detekt +detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" } +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } +# ndregion Detekt + +# region Test +test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutine" } +test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +test-junit = { module = "junit:junit", version.ref = "junit" } +test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } +test-truth = { module = "com.google.truth:truth", version.ref = "truth" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +# endregion Test + +# region Other +amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" } +appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer" } +armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } +coil = { module = "io.coil-kt:coil", version.ref = "coil" } +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } +desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } +googlePlay-core = { module = "com.google.android.play:core", version.ref = "googlePlayCore" } +googlePlay-core-ktx = { module = "com.google.android.play:core-ktx", version.ref = "googlePlayCoreKtx" } +googlePlay-services-wallet = { module = "com.google.android.gms:play-services-wallet", version.ref = "googlePlayServicesWallet" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } +hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-navigation = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +jodatime = { module = "joda-time:joda-time", version.ref = "jodatime" } +krateSharedPref = { module = "hu.autsoft:krate", version.ref = "krateSharedPref" } +kotsonGson = { module = "com.github.salomonbrys.kotson:kotson", version.ref = "kotsonGsonExt" } +lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } +material = { module = "com.google.android.material:material", version.ref = "googleMaterialComponent" } +moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okHttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +otaliastudiosCameraView = { module = "com.otaliastudios:cameraview", version.ref = "otaliastudiosCameraView" } +shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" } +spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } +reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitMoshiConverter" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } +xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } +zendesk-chat = { module = "com.zendesk:chat", version.ref = "zendeskChat" } +zendesk-messaging = { module = "com.zendesk:messaging", version.ref = "zendeskMessaging" } +zxing-qrBarcodeScanner = { module = "me.dm7.barcodescanner:zxing", version.ref = "zxingQrBarcodeScanner" } +zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } +mviCore-watcher = { module = "com.github.badoo.mvicore:mvicore-diff", version.ref = "mviCore" } +kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } +# endregion Other diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9ee3ffb21f..ddde06c17d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 21 13:51:19 MSK 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +#Tue Mar 07 14:45:01 MSK 2023 +distributionBase = GRADLE_USER_HOME +distributionUrl = https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionPath = wrapper/dists +zipStorePath = wrapper/dists +zipStoreBase = GRADLE_USER_HOME diff --git a/libs/auth/build.gradle.kts b/libs/auth/build.gradle.kts index b8dd6f840d..1fbece85d5 100644 --- a/libs/auth/build.gradle.kts +++ b/libs/auth/build.gradle.kts @@ -1,37 +1,5 @@ plugins { - id("com.android.library") - kotlin("android") -} - -android { - - defaultConfig { - compileSdk = AppConfig.compileSdkVersion - minSdk = AppConfig.minSdkVersion - targetSdk = AppConfig.targetSdkVersion - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - isCoreLibraryDesugaringEnabled = false - } - - buildTypes { - create("debug_beta") { - initWith(getByName("release")) - BuildConfigFieldFactory( - fields = listOf( - Field.Environment("release"), - Field.TestActionEnabled(true), - Field.LogEnabled(true), - ), - builder = ::buildConfigField, - ).create() - } - } + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") } \ No newline at end of file diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index de1a6876a4..248cc20bd1 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -1,15 +1,10 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") + alias(deps.plugins.kotlin.jvm) + id("configuration") } dependencies { /** Coroutines */ - implementation(Library.coroutine) -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + implementation(deps.kotlin.coroutines) } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt new file mode 100644 index 0000000000..bdaf360494 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt @@ -0,0 +1,3 @@ +package com.tangem.lib.crypto.models.errors + +class UserCancelledException : Exception() \ No newline at end of file diff --git a/plugins/configuration/.gitignore b/plugins/configuration/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/plugins/configuration/.gitignore @@ -0,0 +1 @@ +/build diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts new file mode 100644 index 0000000000..c2ab7a0a54 --- /dev/null +++ b/plugins/configuration/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `kotlin-dsl` + `java-gradle-plugin` +} + +repositories { + google() + mavenCentral() +} + +configure { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +dependencies { + implementation(deps.gradle.kotlin) + implementation(deps.gradle.android) + implementation(deps.gradle.detekt) +} + +gradlePlugin { + plugins.register("configuration") { + id = "configuration" + implementationClass = "com.tangem.plugin.configuration.ConfigurationPlugin" + } +} \ No newline at end of file diff --git a/config/detekt/detekt.yml b/plugins/configuration/detekt.yml similarity index 100% rename from config/detekt/detekt.yml rename to plugins/configuration/detekt.yml diff --git a/plugins/configuration/settings.gradle.kts b/plugins/configuration/settings.gradle.kts new file mode 100644 index 0000000000..0ed5a2e04b --- /dev/null +++ b/plugins/configuration/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("deps") { + from(files("../../gradle/dependencies.toml")) + } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/ConfigurationPlugin.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/ConfigurationPlugin.kt new file mode 100644 index 0000000000..53b9d53e1c --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/ConfigurationPlugin.kt @@ -0,0 +1,30 @@ +package com.tangem.plugin.configuration + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.internal.plugins.AppPlugin +import com.android.build.gradle.internal.plugins.LibraryPlugin +import com.tangem.plugin.configuration.configurations.configure +import com.tangem.plugin.configuration.configurations.extension.configure +import org.gradle.api.Action +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class ConfigurationPlugin : Plugin { + override fun apply(target: Project) { + target.plugins.all(startConfigurationAction(project = target)) + } + + private fun startConfigurationAction(project: Project) = Action> { + project.configure() + when (this) { + is AppPlugin -> { + project.configure { configure(project) } + } + is LibraryPlugin -> { + project.configure { configure(project) } + } + } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt new file mode 100644 index 0000000000..1bcf190ab5 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt @@ -0,0 +1,52 @@ +package com.tangem.plugin.configuration.configurations + +import com.tangem.plugin.configuration.utils.findLibrary +import com.tangem.plugin.configuration.utils.findPlugin +import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT +import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT_PLUGINS +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType + +internal fun Project.configureDetektRules() { + plugins.apply(findPlugin(alias = CONFIGURATION_DETEKT).pluginId) + extensions.configure { configure(project) } + configureDetektPlugins() + configureDetektTask() +} + +private fun DetektExtension.configure(project: Project) { + parallel = true + ignoreFailures = false + autoCorrect = true + buildUponDefaultConfig = true + config.setFrom(project.rootProject.files("plugins/configuration/detekt.yml")) +} + +private fun Project.configureDetektPlugins() { + listOf( + findLibrary(alias = "detekt-formatting"), + findLibrary(alias = "detekt-compose"), + ).forEach { + dependencies.add(CONFIGURATION_DETEKT_PLUGINS, it) + } +} + +private fun Project.configureDetektTask() { + tasks.withType { + include("**/*.kt") + exclude("**/resources/**", "**/build/**") + reports { + sarif { + required.set(false) + } + txt { + required.set(true) + } + } + + jvmTarget = "11" + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt new file mode 100644 index 0000000000..9d10cad98d --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -0,0 +1,16 @@ +package com.tangem.plugin.configuration.configurations + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +internal fun Project.configureKotlinCompilerOptions() { + project.tasks.withType { + kotlinOptions { + jvmTarget = "11" + allWarningsAsErrors = false + // this is required to produce a unique META-INF/*.kotlin_module files + moduleName = project.path.removePrefix(":").replace(':', '-') + } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt new file mode 100644 index 0000000000..644b1eaead --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt @@ -0,0 +1,8 @@ +package com.tangem.plugin.configuration.configurations + +import org.gradle.api.Project + +internal fun Project.configure() { + configureKotlinCompilerOptions() + configureDetektRules() +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt new file mode 100644 index 0000000000..1ba8b81a2d --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -0,0 +1,96 @@ +package com.tangem.plugin.configuration.configurations.extension + +import com.android.build.gradle.AppExtension +import com.tangem.plugin.configuration.model.AppConfig +import com.tangem.plugin.configuration.model.BuildType +import com.tangem.plugin.configuration.utils.BuildConfigFieldFactory +import org.gradle.api.Project +import com.android.build.gradle.internal.dsl.BuildType as AndroidBuildType + +internal fun AppExtension.configure(project: Project) { + configureCompileSdk() + configureDefaultConfig(project) + configureBuildFeatures() + configureBuildTypes() + configurePackagingOptions() + configureCompose(project) + configureCompilerOptions() +} + +private fun AppExtension.configureDefaultConfig(project: Project) { + defaultConfig { + applicationId = AppConfig.packageName + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + + versionCode = if (project.hasProperty("versionCode")) { + (project.property("versionCode") as String).toInt() + } else { + AppConfig.versionCode + } + + versionName = if (project.hasProperty("versionName")) { + project.property("versionName") as String + } else { + AppConfig.versionName + } + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +// TODO: [REDACTED_JIRA] +private fun AppExtension.configureBuildFeatures() { + buildFeatures.apply { + viewBinding = true + } +} + +private fun AppExtension.configureBuildTypes() { + buildTypes { + BuildType.values().forEach { buildVariant -> + maybeCreate(buildVariant.id).apply { + configureBuildVariant(extension = this@configureBuildTypes, buildVariant) + + BuildConfigFieldFactory( + fields = buildVariant.configFields, + builder = ::buildConfigField, + ).create() + } + } + } +} + +private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) { + when (buildType) { + BuildType.Release -> { + isDebuggable = false + isMinifyEnabled = false + proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + BuildType.Debug -> { + isDebuggable = true + isMinifyEnabled = false + } + BuildType.Internal, + BuildType.External, + -> { + initWith(extension.buildTypes.getByName(BuildType.Release.id)) + signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + } + } + + versionNameSuffix = buildType.versionSuffix?.let { "-$it" } + applicationIdSuffix = buildType.appIdSuffix?.let { ".$it" } +} + +private fun AppExtension.configurePackagingOptions() { + packagingOptions { + resources { + excludes += "lib/x86_64/darwin/libscrypt.dylib" + excludes += "lib/x86_64/freebsd/libscrypt.so" + excludes += "lib/x86_64/linux/libscrypt.so" + excludes += "META-INF/gradle/incremental.annotation.processors" + } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt new file mode 100644 index 0000000000..5d0b3a45e4 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -0,0 +1,33 @@ +package com.tangem.plugin.configuration.configurations.extension + +import com.android.build.gradle.BaseExtension +import com.tangem.plugin.configuration.model.AppConfig +import com.tangem.plugin.configuration.utils.findVersion +import org.gradle.api.JavaVersion +import org.gradle.api.Project + +internal fun BaseExtension.configureCompileSdk() { + compileSdkVersion(AppConfig.compileSdkVersion) +} + +internal fun BaseExtension.configureCompilerOptions() { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +internal fun BaseExtension.configureCompose(project: Project) { + val useCompose = with(project.path) { + contains(":ui") || + contains(":presentation") || + contains(":app") || // TODO: [REDACTED_JIRA] + contains(":tester:impl") // TODO: Rename module + } + buildFeatures.compose = useCompose + if (useCompose) { + composeOptions { + kotlinCompilerExtensionVersion = project.findVersion(alias = "compose-compiler").requiredVersion + } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt new file mode 100644 index 0000000000..bc18afa9b9 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -0,0 +1,49 @@ +package com.tangem.plugin.configuration.configurations.extension + +import com.android.build.gradle.LibraryExtension +import com.tangem.plugin.configuration.model.AppConfig +import com.tangem.plugin.configuration.model.BuildType +import com.tangem.plugin.configuration.utils.BuildConfigFieldFactory +import org.gradle.api.Project + +internal fun LibraryExtension.configure(project: Project) { + configureCompileSdk() + configureDefaultConfig() + configureBuildTypes() + configurePackagingOptions() + configureCompose(project) + configureCompilerOptions() +} + +private fun LibraryExtension.configureDefaultConfig() { + defaultConfig { + minSdk = AppConfig.minSdkVersion + vectorDrawables { + useSupportLibrary = true + } + } +} + +private fun LibraryExtension.configureBuildTypes() { + buildTypes { + BuildType.values().forEach { buildVariant -> + maybeCreate(buildVariant.id).apply { + BuildConfigFieldFactory( + fields = buildVariant.configFields, + builder = ::buildConfigField, + ).create() + } + } + } +} + +private fun LibraryExtension.configurePackagingOptions() { + packagingOptions { + resources { + excludes += "lib/x86_64/darwin/libscrypt.dylib" + excludes += "lib/x86_64/freebsd/libscrypt.so" + excludes += "lib/x86_64/linux/libscrypt.so" + excludes += "META-INF/gradle/incremental.annotation.processors" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt similarity index 60% rename from buildSrc/src/main/java/AppConfig.kt rename to plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 9a7e91768d..7f12be0c15 100644 --- a/buildSrc/src/main/java/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -1,7 +1,9 @@ -object AppConfig { +package com.tangem.plugin.configuration.model + +internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 - const val versionName = "1.0.0.-SNAPSHOT" + const val versionName = "1.0.0-SNAPSHOT" const val minSdkVersion = 21 const val targetSdkVersion = 33 const val compileSdkVersion = 33 diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt new file mode 100644 index 0000000000..66437a1de5 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -0,0 +1,35 @@ +package com.tangem.plugin.configuration.model + +/** + * Build config field + * + * @property type field type + * @property name field name + * @property value field value + */ +internal sealed class BuildConfigField(val type: String, val name: String, val value: String) { + + class Environment(value: String) : BuildConfigField( + type = "String", + name = "ENVIRONMENT", + value = "\"$value\"", + ) + + class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "TEST_ACTION_ENABLED", + value = isEnabled.toString(), + ) + + class LogEnabled(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "LOG_ENABLED", + value = isEnabled.toString(), + ) + + class TesterMenuAvailability(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "TESTER_MENU_ENABLED", + value = isEnabled.toString(), + ) +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt new file mode 100644 index 0000000000..b872eb35ff --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -0,0 +1,93 @@ +package com.tangem.plugin.configuration.model + +internal enum class BuildType( + val id: String, + val appIdSuffix: String? = null, + val versionSuffix: String? = null, + val configFields: List, +) { + + /** + * Build type for developers + * + * Features: + * - Env: dev + * - Signing config: debug + * - Debuggable + * - Logs + * - Tester menu + * - Test action + * - Traffic sniffing + * */ + Debug( + id = "debug", + appIdSuffix = "debug", + configFields = listOf( + BuildConfigField.Environment(value = "dev"), + BuildConfigField.TestActionEnabled(isEnabled = true), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = true), + ), + ), + + /** + * Build type for QA + * + * Features: + * - Env: beta + * - Signing config: debug + * - Logs + * - Tester menu + * - Test action + * - Traffic sniffing + * */ + Internal( + id = "internal", + appIdSuffix = "internal", + versionSuffix = "internal", + configFields = listOf( + BuildConfigField.Environment(value = "beta"), + BuildConfigField.TestActionEnabled(isEnabled = true), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = true), + ), + ), + + /** + * Build type for QA and business + * + * Features: + * - Env: prod + * - Signing config: debug + * - Proguard + * */ + External( + id = "external", + appIdSuffix = "external", + versionSuffix = "external", + configFields = listOf( + BuildConfigField.Environment(value = "prod"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = false), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + ), + ), + + /** + * Production ready build type for clients + * + * Features: + * - Env: prod + * - Signing config: release + * - Proguard + * */ + Release( + id = "release", + configFields = listOf( + BuildConfigField.Environment(value = "prod"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = false), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + ), + ), +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/BuildConfigFieldFactory.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/BuildConfigFieldFactory.kt new file mode 100644 index 0000000000..66485abb14 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/BuildConfigFieldFactory.kt @@ -0,0 +1,14 @@ +package com.tangem.plugin.configuration.utils + +import com.tangem.plugin.configuration.model.BuildConfigField + +private typealias ConfigFieldBuilder = (String, String, String) -> Unit + +internal class BuildConfigFieldFactory( + private val fields: List, + private val builder: ConfigFieldBuilder, +) { + fun create() { + fields.forEach { field -> builder(field.type, field.name, field.value) } + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionCatalogsUtils.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionCatalogsUtils.kt new file mode 100644 index 0000000000..78cb30e988 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionCatalogsUtils.kt @@ -0,0 +1,28 @@ +package com.tangem.plugin.configuration.utils + +import org.gradle.api.Project +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.artifacts.VersionConstraint +import org.gradle.kotlin.dsl.getByType +import org.gradle.plugin.use.PluginDependency +import kotlin.jvm.optionals.getOrNull + +private val Project.depsVersionCatalog: VersionCatalog + get() = extensions.getByType().named("deps") + +internal fun Project.findVersion(alias: String): VersionConstraint { + return depsVersionCatalog.findVersion(alias).getOrNull() + ?: error("Unable to find version $alias") +} + +internal fun Project.findPlugin(alias: String): PluginDependency { + return depsVersionCatalog.findPlugin(alias).getOrNull()?.orNull + ?: error("Unable to find plugin $alias") +} + +internal fun Project.findLibrary(alias: String): MinimalExternalModuleDependency { + return depsVersionCatalog.findLibrary(alias).getOrNull()?.orNull + ?: error("Unable to find library $alias") +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 75add91dd0..dbb3202511 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,33 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } + + includeBuild("plugins/configuration") +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositories { + google() + mavenCentral() + mavenLocal() + jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview + maven("https://nexus.tangem-tech.com/repository/maven-releases/") + maven("https://jitpack.io") + maven("https://zendesk.jfrog.io/zendesk/repo") + } + + versionCatalogs { + create("deps") { + from(files("gradle/dependencies.toml")) + } + } +} + include(":app") include(":domain") include(":common") @@ -5,6 +35,7 @@ include(":common") // region Core modules include(":core:analytics") include(":core:datasource") +include(":core:featuretoggles") include(":core:res") include(":core:utils") include(":core:ui") @@ -19,9 +50,11 @@ include(":libs:auth") include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") +include(":features:swap:api") include(":features:swap:data") include(":features:swap:domain") include(":features:swap:presentation") +include(":features:tester:api") include(":features:tester:impl") // endregion Feature modules \ No newline at end of file