diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 218986fc34..e68e3543e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("kapt") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") + id("com.google.dagger.hilt.android") } android { @@ -95,6 +96,7 @@ android { 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" } } @@ -107,6 +109,13 @@ dependencies { implementation(project(":domain")) implementation(project(":network")) implementation(project(":common")) + implementation(project(":core:ui")) + implementation(project(":libs:crypto")) + + /** Features */ + implementation(project(":features:referral:presentation")) + implementation(project(":features:referral:domain")) + implementation(project(":features:referral:data")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) @@ -134,9 +143,17 @@ dependencies { implementation(Firebase.firebaseCrashlytics) /** Tangem libraries */ - implementation(Tangem.blockchain) + implementation(Tangem.blockchain) { + exclude(module = "joda-time") + } implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) + implementation(Tangem.cardAndroid) { + exclude(module = "joda-time") + } + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) /** Other libraries */ implementation(Library.materialComponent) @@ -169,7 +186,7 @@ dependencies { } implementation(Library.accompanistAppCompatTheme) implementation(Library.accompanistSystemUiController) - implementation(Library.skydovesVeil) + implementation(Library.xmlShimmer) implementation(Library.viewBindingDelegate) implementation(Library.armadillo) implementation(Library.googlePlayServicesWallet) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index e557e59247..aa9e63c2dc 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -30,6 +30,7 @@ import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -49,6 +50,7 @@ private val mainCoroutineContext: CoroutineContext get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) +@AndroidEntryPoint class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { private var snackbar: Snackbar? = null diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 5074f09e8d..d5491a6088 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -43,6 +43,7 @@ import com.tangem.tap.persistence.CardBalanceStateAdapter import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig import com.zendesk.logger.Logger +import dagger.hilt.android.HiltAndroidApp import org.rekotlin.Store import timber.log.Timber import zendesk.chat.Chat @@ -60,6 +61,7 @@ lateinit var shopService: TangemShopService lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository +@HiltAndroidApp class TapApplication : Application(), ImageLoaderFactory { override fun onCreate() { diff --git a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt index 88b088fa6a..2013295f25 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt @@ -9,13 +9,13 @@ import com.tangem.wallet.R object TangemTypography { val body1 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 16.0.sp, letterSpacing = 0.5.sp, lineHeight = 24.0.sp, ) val body2 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 14.0.sp, letterSpacing = 0.25.sp, lineHeight = 20.0.sp, @@ -27,13 +27,13 @@ object TangemTypography { lineHeight = 20.0.sp, ) val caption = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 12.0.sp, letterSpacing = 0.4000000059604645.sp, lineHeight = 16.0.sp, ) val headline1 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 34.0.sp, letterSpacing = 0.0.sp, lineHeight = 44.0.sp, 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 6ccf1b9b58..7b2322f79d 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 @@ -184,7 +184,7 @@ class TokensMiddleware { } } - private fun deriveMissingBlockchains( + fun deriveMissingBlockchains( scanResponse: ScanResponse, currencyList: List, onSuccess: (ScanResponse) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt new file mode 100644 index 0000000000..eb4bfba228 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.proxy + +import com.tangem.common.card.Card +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.features.tokens.redux.TokensMiddleware +import com.tangem.tap.features.wallet.redux.WalletState +import org.rekotlin.Store + +/** + * Holds objects from old modules, that missing in DI graph + * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI + */ +class AppStateHolder { + + val scanResponse: ScanResponse? = null + val walletState: WalletState? = null + val userTokensRepository: UserTokensRepository? = null + val mainStore: Store? = null + val tokesMiddleware: TokensMiddleware? = null + + fun getActualCard(): Card? { + return scanResponse?.card + } +} \ 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 new file mode 100644 index 0000000000..e27bfd5238 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -0,0 +1,60 @@ +package com.tangem.tap.proxy + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.domain.common.TapWorkarounds.derivationStyle +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.NonNativeToken +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import kotlin.coroutines.suspendCoroutine + +class DerivationManagerImpl( + private val appStateHolder: AppStateHolder, +) : DerivationManager { + + override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> + val tokesMiddleware = requireNotNull(appStateHolder.tokesMiddleware) { "tokesMiddleware is null" } + val blockchain = Blockchain.fromNetworkId(currency.networkId) + val card = appStateHolder.getActualCard() + if (blockchain != null && card != null) { + val appToken = if (currency is NonNativeToken) { + Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + ) + } else null + val blockchainNetwork = BlockchainNetwork(blockchain, card) + val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork( + blockchainNetwork, + appToken, + ) + if (appStateHolder.scanResponse != null) { + tokesMiddleware.deriveMissingBlockchains( + scanResponse = appStateHolder.scanResponse, + listOf(appCurrency), + ) { + continuation.resumeWith(Result.success(true)) + } + } + } else { + continuation.resumeWith(Result.failure(IllegalStateException("no blockchain or card found"))) + } + } + + override fun hasDerivation(networkId: String): Boolean { + val scanResponse = appStateHolder.scanResponse + val blockchain = Blockchain.fromNetworkId(networkId) + if (scanResponse != null && blockchain != null) { + val derivationPath = blockchain.derivationPath(appStateHolder.getActualCard()?.derivationStyle)?.rawPath + if (derivationPath.isNullOrEmpty()) return false + return scanResponse.hasDerivation( + blockchain, + derivationPath, + ) + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt new file mode 100644 index 0000000000..1a7f3fbcd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -0,0 +1,152 @@ +package com.tangem.tap.proxy + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationParams +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.Currency +import com.tangem.lib.crypto.models.NativeToken +import com.tangem.lib.crypto.models.NonNativeToken +import com.tangem.tap.domain.extensions.getUserWalletId +import com.tangem.tap.domain.extensions.makeWalletManagerForApp +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.redux.WalletAction +import org.rekotlin.Action + +class UserWalletManagerImpl( + private val appStateHolder: AppStateHolder, + private val walletManagerFactory: WalletManagerFactory, +) : UserWalletManager { + + override suspend fun getUserTokens(): List { + val card = appStateHolder.getActualCard() + val userTokensRepository = + requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } + if (card != null) { + userTokensRepository.getUserTokens(card) + .filter { it.isToken() } + .map { + if (it is com.tangem.tap.features.wallet.models.Currency.Token) { + NonNativeToken( + id = it.coinId ?: "", + name = it.currencyName, + symbol = it.currencySymbol, + networkId = it.blockchain.toNetworkId(), + contractAddress = it.token.contractAddress, + decimalCount = it.token.decimals, + ) + } else { + NativeToken( + id = it.coinId ?: "", + name = it.currencyName, + symbol = it.currencySymbol, + networkId = it.blockchain.toNetworkId(), + ) + } + } + } + return emptyList() + } + + override fun getWalletId(): String { + return appStateHolder.getActualCard()?.getUserWalletId() ?: "" + } + + override suspend fun isTokenAdded(currency: Currency): Boolean { + val card = appStateHolder.getActualCard() + val userTokensRepository = + requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } + if (card != null) { + return userTokensRepository.getUserTokens(card) + .any { it.coinId.equals(currency.id) } //todo ensure that its the same ids + } + return false + } + + //todo check is it sync operation + override fun addToken(currency: Currency) { + val action = if (currency is NativeToken) { + val card = appStateHolder.getActualCard() + if (card != null) { + addNativeTokenToWalletAction(currency, card) + } else { + throw IllegalStateException("card not found") + } + } else { + addNonNativeTokenToWalletAction(currency as NonNativeToken) + } + val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" } + mainStore.dispatch(action) + } + + override fun getWalletAddress(currency: Currency): String { + val blockchain = Blockchain.fromNetworkId(currency.networkId) + val card = appStateHolder.getActualCard() + if (blockchain != null && card != null) { + val blockchainNetwork = BlockchainNetwork(blockchain, card) + val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + if (walletManager != null) { + return walletManager.wallet.address + } else { + throw IllegalStateException("no wallet manager found") + } + } else { + throw IllegalStateException("no blockchain or card found") + } + } + + private fun addNativeTokenToWalletAction(token: NativeToken, card: Card): Action { + val blockchain = Blockchain.fromNetworkId(token.networkId) + val scanResponse = appStateHolder.scanResponse + if (blockchain != null && scanResponse != null) { + val blockchainNetwork = BlockchainNetwork(blockchain, card) + var walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + if (walletManager == null) { + walletManager = walletManagerFactory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = blockchain, + derivationParams = createDerivationParams(card.derivationStyle), + ) + } + return WalletAction.MultiWallet.AddBlockchain( + blockchain = blockchainNetwork, + walletManager = walletManager, + save = true, + ) + } else { + throw IllegalStateException("blockchain is not supported") + } + } + + private fun addNonNativeTokenToWalletAction(token: NonNativeToken): Action { + val card = appStateHolder.getActualCard() + val blockchain = Blockchain.fromNetworkId(token.networkId) + if (card != null && blockchain != null) { + return WalletAction.MultiWallet.AddToken( + token = com.tangem.blockchain.common.Token( + id = token.id, + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimalCount, + ), + blockchain = BlockchainNetwork( + blockchain, + card, + ), + save = true, + ) + } else { + throw IllegalStateException("no card or blockchain found") + } + } + + private fun createDerivationParams(derivationStyle: DerivationStyle?): DerivationParams? { + return derivationStyle?.let { DerivationParams.Default(derivationStyle) } //todo clarify if its need to add Custom + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt new file mode 100644 index 0000000000..07d3fb1e8d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.proxy.di + +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.DerivationManagerImpl +import com.tangem.tap.proxy.UserWalletManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ProxyModule { + + @Provides + @Singleton + fun provideAppStateHolder(): AppStateHolder { + return AppStateHolder() + } + + @Provides + @Singleton + fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager { + return UserWalletManagerImpl( + appStateHolder = appStateHolder, + walletManagerFactory = WalletManagerFactory(), + ) + } + + @Provides + @Singleton + fun provideDerivationManager(appStateHolder: AppStateHolder): DerivationManager { + return DerivationManagerImpl( + appStateHolder = appStateHolder, + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_more_vertical_24.xml b/app/src/main/res/drawable/ic_more_vertical_24.xml deleted file mode 100644 index fd51d1fbf2..0000000000 --- a/app/src/main/res/drawable/ic_more_vertical_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/menu/menu_wallet.xml b/app/src/main/res/menu/menu_wallet.xml index edd88afb93..c5848ecb6a 100644 --- a/app/src/main/res/menu/menu_wallet.xml +++ b/app/src/main/res/menu/menu_wallet.xml @@ -2,9 +2,9 @@ + android:id="@+id/details_menu" + android:title="@string/details_title" + android:icon="@drawable/ic_more_vertical" + app:iconTint="@color/darkGray6" + app:showAsAction="ifRoom" /> diff --git a/build.gradle.kts b/build.gradle.kts index 0cc8e9c419..75b1a4e453 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,10 @@ buildscript { } } +plugins { + id("com.google.dagger.hilt.android") version "2.44" apply false +} + allprojects { repositories { google() diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index 25c0e3787a..f93b728008 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -34,12 +34,16 @@ object Library { 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 hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt + const val jodatime = "net.danlew:android.joda:" + 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 @@ -49,7 +53,6 @@ object Library { 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 skydovesVeil = "com.github.skydoves:androidveil:" + Versions.skydovesAndroidVeil 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 @@ -57,6 +60,7 @@ object Library { 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 diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index d7d89a1fcd..54b4716dc7 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -28,6 +28,7 @@ object Versions { 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" @@ -35,6 +36,8 @@ object Versions { 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" @@ -44,10 +47,10 @@ object Versions { const val retrofit = "2.8.1" const val retrofitMoshiConverter = "2.6.0" const val shopifyBuySdk = "12.0.0" - const val skydovesAndroidVeil = "1.1.2" const val spongycastleCryptoCore = "1.58.0.0" const val timber = "4.7.1" const val viewBindingDelegate = "1.5.6" + const val xmlShimmer = "1.1.2" const val zendeskChat = "3.3.5" const val zendeskMessaging = "5.2.4" const val zxingQrBarcodeScanner = "1.9.8" diff --git a/core/datasource/.gitignore b/core/datasource/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/datasource/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts new file mode 100644 index 0000000000..7d8f651682 --- /dev/null +++ b/core/datasource/build.gradle.kts @@ -0,0 +1,48 @@ +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 + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) + + /** Coroutines */ + implementation(Library.coroutine) + + /** Logging */ + implementation(Library.timber) + + /** Network */ + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.okHttp) + implementation(Library.okHttpLogging) + + /** Time */ + implementation(Library.jodatime) +} \ No newline at end of file diff --git a/core/datasource/src/main/AndroidManifest.xml b/core/datasource/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d6401c6174 --- /dev/null +++ b/core/datasource/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt new file mode 100644 index 0000000000..e7eaf31343 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.referral + +import com.tangem.datasource.api.referral.models.ReferralResponse +import com.tangem.datasource.api.referral.models.StartReferralBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +/** + * Api for referral feature + */ +interface +ReferralApi { + + /** Returns referral status by [walletId] */ + @GET("referral/{walletId}") + suspend fun getReferralStatus(@Query("name") walletId: String): ReferralResponse + + /** Make user referral, requires [StartReferralBody] */ + @POST("referral") + suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt new file mode 100644 index 0000000000..a21aee2286 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt @@ -0,0 +1,51 @@ +package com.tangem.datasource.api.referral.models + +import com.squareup.moshi.Json +import org.joda.time.DateTime + +/** + * Main response class for referral API + * contains all necessary info about users program status + */ +data class ReferralResponse( + @Json(name = "conditions") val conditions: Conditions, + @Json(name = "referral") val referral: Referral?, +) + +data class Conditions( + @Json(name = "award") val award: Int, + @Json(name = "discount") val discount: Discount, + @Json(name = "awards") val awards: List, + @Json(name = "tosLink") val tosLink: String, +) + +data class Discount( + @Json(name = "amount") val amount: Int, + @Json(name = "discountType") val discountType: DiscountType, +) + +data class Award( + @Json(name = "amount") val amount: Int, + @Json(name = "token") val token: Token, +) + +data class Token( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "networkId") val networkId: String, + @Json(name = "contractAddress") val contractAddress: String?, + @Json(name = "decimalCount") val decimalCount: Int?, +) + +data class Referral( + @Json(name = "shareLink") val shareLink: String, + @Json(name = "address") val address: String, + @Json(name = "promocode") val promocode: String, + @Json(name = "walletsPurchased") val walletsPurchased: Int, + @Json(name = "termsAcceptedAt") val termsAcceptedAt: DateTime?, +) + +enum class DiscountType { + PERCENTAGE, VALUE +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt new file mode 100644 index 0000000000..98a0a8ab90 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.referral.models + +import com.squareup.moshi.Json + +/** Body for make user referral */ +data class StartReferralBody( + @Json(name = "walletId") val walletId: String, + @Json(name = "networkId") val networkId: String, + @Json(name = "tokenId") val tokenId: String, + @Json(name = "address") val address: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt new file mode 100644 index 0000000000..823d3e63d8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.api.referral.ReferralApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NetworkModule { + + @Provides + @Singleton + fun provideReferralApi(okHttpClient: OkHttpClient): ReferralApi { + return Retrofit.Builder() + .addConverterFactory( + MoshiConverterFactory.create(), + ) + .baseUrl(PROD_REFERRAL_BASE_URL) + .client(okHttpClient) + .build() + .create(ReferralApi::class.java) + } + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC), + ) + .build() + } + + private companion object { + const val PROD_REFERRAL_BASE_URL = "" + } +} \ No newline at end of file diff --git a/core/ui/.gitignore b/core/ui/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts new file mode 100644 index 0000000000..3c509f8c3f --- /dev/null +++ b/core/ui/build.gradle.kts @@ -0,0 +1,41 @@ +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 + } +} + +dependencies { + + /** Compose */ + implementation(Compose.foundation) + implementation(Compose.material) + implementation(Compose.uiTooling) + + /** Other libraries */ + implementation(Library.accompanistSystemUiController) +} \ No newline at end of file diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c5a36c7e17 --- /dev/null +++ b/core/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt new file mode 100644 index 0000000000..65b331e15c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -0,0 +1,183 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.ButtonColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.buttonColor +import com.tangem.core.ui.res.textColor + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +/** + * Primary button with an icon at the beginning of the layout + * + * @param modifier button modifier + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Composable +fun PrimaryStartIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Text(text = text) + } +} + +/** + * Primary button with an icon at the end of the layout + * + * @param modifier button modifier + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Composable +fun PrimaryEndIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { + Text(text = text) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + } +} + +@Composable +private fun PrimaryButtonRow( + modifier: Modifier, + enabled: Boolean, + onClicked: () -> Unit, + content: @Composable (RowScope.() -> Unit), +) { + Button( + onClick = onClicked, + modifier = modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.size48)), + enabled = enabled, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), + contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), + disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), + disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), + ), + content = content, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt new file mode 100644 index 0000000000..57a47dff46 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -0,0 +1,79 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_SimpleInfoCard_InLightTheme() { + TangemTheme(isDarkTheme = false) { + SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_SimpleInfoCard_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") + } +} + +/** + * Small card with text information attached to the edges + * + * @param startText text information attached to the left edge + * @param endText text information attached to the right edge + * + * @see Figma component + */ +@Composable +fun SmallInfoCard(startText: String, endText: String) { + Surface( + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + color = MaterialTheme.colors.secondary, + elevation = dimensionResource(id = R.dimen.elevation2), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.size48)) + .padding( + horizontal = dimensionResource(id = R.dimen.spacing16), + vertical = dimensionResource(id = R.dimen.spacing12), + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = startText, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + maxLines = 1, + style = MaterialTheme.typography.subtitle2 + ) + Text( + text = endText, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.body2 + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt new file mode 100644 index 0000000000..60ce6bf8bc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DimenRes +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource + +@Composable +fun VerticalSpacer(@DimenRes spaceResId: Int) { + Spacer(modifier = Modifier.height(dimensionResource(id = spaceResId))) +} + +@Composable +fun HorizontalSpacer(@DimenRes spaceResId: Int) { + Spacer(modifier = Modifier.width(dimensionResource(id = spaceResId))) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt new file mode 100644 index 0000000000..73cca24307 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -0,0 +1,169 @@ +package com.tangem.core.ui.components.appbar + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.iconColor +import com.tangem.core.ui.res.textColor + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: String, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.size56)) + .padding(all = dimensionResource(id = R.dimen.spacing16)), + ) { + if (startButton != null) { + IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size24)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + } + + Text( + text = text, + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.subtitle1, + ) + + if (endButton != null) { + IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size24)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithAdditionalButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyEndButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt new file mode 100644 index 0000000000..99d6aa527a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -0,0 +1,72 @@ +package com.tangem.core.ui.components.appbar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun PreviewAppBarWithBackButtonInLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithBackButton(text = "Title", onBackClick = {}) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun PreviewAppBarWithBackButtonInDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithBackButton(text = "Title", onBackClick = {}) + } +} + +/** + * App bar with back button and optional title + * + * @param text optional title + * @param onBackClick the lambda to be invoked when this icon is pressed + * + * @see Figma component + */ +@Composable +fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) { + Row( + modifier = Modifier + .padding(all = dimensionResource(R.dimen.spacing16)) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing16)), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.ic_back), + contentDescription = null, + modifier = Modifier + .size(size = dimensionResource(R.dimen.size24)) + .clickable { onBackClick() }, + tint = MaterialTheme.colors.onPrimary, + ) + if (!text.isNullOrBlank()) { + Text( + text = text, + color = MaterialTheme.colors.onPrimary, + maxLines = 1, + style = MaterialTheme.typography.subtitle1, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt new file mode 100644 index 0000000000..2517f5ef41 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.components.appbar.models + +import androidx.annotation.DrawableRes + +data class AdditionalButton( + @DrawableRes val iconRes: Int, + val onIconClicked: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt new file mode 100644 index 0000000000..edf127e645 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt @@ -0,0 +1,60 @@ +package com.tangem.core.ui.components.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.iconColor + +/** + * Bottom sheet indicator + * + * @see + * Figma Component + */ +@Composable +fun BottomSheetIndicator() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.size20)), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .width(dimensionResource(id = R.dimen.size32)) + .height(dimensionResource(id = R.dimen.size4)) + .background( + color = MaterialTheme.colors.iconColor(type = IconColorType.INACTIVE), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius2)), + ), + ) + } +} + +@Preview(heightDp = 20, showBackground = true) +@Composable +fun Preview_BottomSheetIndicator_InLightTheme() { + TangemTheme(isDarkTheme = false) { + BottomSheetIndicator() + } +} + +@Preview(heightDp = 20, showBackground = true) +@Composable +fun Preview_BottomSheetIndicator_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + BottomSheetIndicator() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt new file mode 100644 index 0000000000..cb6607c8a3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class ButtonColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY(lightColor = Dark6, darkColor = Light1), + SECONDARY(lightColor = Light2, darkColor = Dark5), + DISABLED(lightColor = Light2, darkColor = Dark6), + POSITIVE(lightColor = Meadow, darkColor = Meadow), + POSITIVE_DISABLED(lightColor = MagicMint, darkColor = DarkGreen) +} + +@Composable +fun Colors.buttonColor(type: ButtonColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt b/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt new file mode 100644 index 0000000000..4d37ecee45 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt @@ -0,0 +1,45 @@ +package com.tangem.core.ui.res + +import androidx.compose.ui.graphics.Color + +// region Base +val Black = Color(0xFF000000) +val White = Color(0xFFFFFFFF) +// endregion Base + +// region Dark +val Dark1 = Color(0xFF919191) +val Dark2 = Color(0xFF656565) +val Dark3 = Color(0xFF494949) +val Dark4 = Color(0xFF3B3B3B) +val Dark5 = Color(0xFF303030) +val Dark6 = Color(0xFF1E1E1E) +// endregion Dark + +// region Light +val Light1 = Color(0xFFF5F5F5) +val Light2 = Color(0xFFEBEBEB) +val Light3 = Color(0xFFD3D3D3) +val Light4 = Color(0xFFC9C9C9) +val Light5 = Color(0xFFB0B0B0) +// endregion Light + +// region Green +val Meadow = Color(0xFF1ACE80) +val MagicMint = Color(0xFFA3EBCC) +val DarkGreen = Color(0xFF06311F) +// endregion Green + +// region Blue +val Cobalt = Color(0xFF0029FF) +val Azure = Color(0xFF007AFF) +// endregion Blue + +// region Red +val Amaranth = Color(0xFFDE1010) +val Flamingo = Color(0xFFED7979) +// endregion Red + +// region Yellow +val Tangerine = Color(0xFFFFB71B) +// endregion Yellow \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt new file mode 100644 index 0000000000..72cc8936bb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class IconColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY1(lightColor = Black, darkColor = White), + PRIMARY2(lightColor = White, darkColor = Dark6), + SECONDARY(lightColor = Dark2, darkColor = Dark1), + INFORMATIVE(lightColor = Light5, darkColor = Dark2), + INACTIVE(lightColor = Light4, darkColor = Dark4), + ACCENT(lightColor = Meadow, darkColor = Meadow), + WARNING(lightColor = Amaranth, darkColor = Amaranth), + ATTENTION(lightColor = Tangerine, darkColor = Tangerine) +} + +@Composable +fun Colors.iconColor(type: IconColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt new file mode 100644 index 0000000000..ebd0da2f57 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -0,0 +1,53 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +internal val isSystemInDarkTheme: Boolean = false // TODO: use isSystemInDarkTheme() for automatic color detection + +@Composable +fun TangemTheme( + isDarkTheme: Boolean = isSystemInDarkTheme, + content: @Composable () -> Unit, +) { + val systemUiController = rememberSystemUiController() + SideEffect { + systemUiController.setSystemBarsColor(color = if (isDarkTheme) Black else Light1, darkIcons = !isDarkTheme) + } + + MaterialTheme( + colors = if (isDarkTheme) DarkColors else LightColors, + typography = TangemTypography, + content = content, + ) +} + +private val LightColors = lightColors( + primary = Light1, + primaryVariant = Light1, + secondary = White, + background = Light1, + surface = White, + error = Amaranth, + onPrimary = Dark6, + onSecondary = Dark6, + onBackground = Dark6, + onSurface = Dark6, +) + +private val DarkColors = darkColors( + primary = Black, + primaryVariant = Black, + secondary = Dark6, + background = Black, + surface = Dark6, + error = Amaranth, + onPrimary = White, + onSecondary = White, + onBackground = White, + onSurface = White, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt new file mode 100644 index 0000000000..89ec283924 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -0,0 +1,82 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.ExperimentalUnitApi +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.R + +private val RobotoFamily = FontFamily( + Font(R.font.roboto_regular, FontWeight.Normal), + Font(R.font.roboto_medium, FontWeight.Medium), +) + +@OptIn(ExperimentalUnitApi::class) +val TangemTypography = Typography( + defaultFontFamily = RobotoFamily, + h1 = TextStyle( + fontSize = 34.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0f, TextUnitType.Sp), + lineHeight = TextUnit(44f, TextUnitType.Sp), + ), + h2 = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.18f, TextUnitType.Sp), + lineHeight = TextUnit(32f, TextUnitType.Sp), + ), + h3 = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.15f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + subtitle1 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.15f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + subtitle2 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.5f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + body1 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.5f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + body2 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.25f, TextUnitType.Sp), + lineHeight = TextUnit(20f, TextUnitType.Sp), + ), + button = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.1f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), + caption = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.4f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), + overline = TextStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(1.5f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt new file mode 100644 index 0000000000..44f94e1157 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class TextColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY1(lightColor = Dark6, darkColor = White), + PRIMARY2(lightColor = White, darkColor = Dark6), + SECONDARY(lightColor = Dark2, darkColor = Light5), + TERTIARY(lightColor = Dark1, darkColor = Dark1), + DISABLED(lightColor = Light4, darkColor = Dark3), + ACCENT(lightColor = Meadow, darkColor = Meadow), + WARNING(lightColor = Amaranth, darkColor = Amaranth), + ATTENTION(lightColor = Tangerine, darkColor = Tangerine), + CONSTANT_WHITE(lightColor = White, darkColor = White) +} + +@Composable +fun Colors.textColor(type: TextColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/core/ui/src/main/res/drawable/ic_back.xml similarity index 100% rename from app/src/main/res/drawable/ic_back.xml rename to core/ui/src/main/res/drawable/ic_back.xml diff --git a/core/ui/src/main/res/drawable/ic_copy.xml b/core/ui/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000000..d01cdf9823 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_discount.xml b/core/ui/src/main/res/drawable/ic_discount.xml new file mode 100644 index 0000000000..5045eccd5b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_discount.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_more_vertical.xml b/core/ui/src/main/res/drawable/ic_more_vertical.xml new file mode 100644 index 0000000000..9f5887ceb4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_vertical.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_scan.xml b/core/ui/src/main/res/drawable/ic_scan.xml similarity index 94% rename from app/src/main/res/drawable/ic_scan.xml rename to core/ui/src/main/res/drawable/ic_scan.xml index 1e4cd949d6..31f4345be7 100644 --- a/app/src/main/res/drawable/ic_scan.xml +++ b/core/ui/src/main/res/drawable/ic_scan.xml @@ -2,7 +2,7 @@ android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - + diff --git a/core/ui/src/main/res/drawable/ic_share.xml b/core/ui/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000..e35c52d223 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tangem.xml b/core/ui/src/main/res/drawable/ic_tangem.xml similarity index 100% rename from app/src/main/res/drawable/ic_tangem.xml rename to core/ui/src/main/res/drawable/ic_tangem.xml diff --git a/core/ui/src/main/res/drawable/ic_tether.xml b/core/ui/src/main/res/drawable/ic_tether.xml new file mode 100644 index 0000000000..e3f94b5dbb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tether.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ill_businessman_3d.webp b/core/ui/src/main/res/drawable/ill_businessman_3d.webp new file mode 100644 index 0000000000..a018c1b5bb Binary files /dev/null and b/core/ui/src/main/res/drawable/ill_businessman_3d.webp differ diff --git a/core/ui/src/main/res/font/roboto_medium.ttf b/core/ui/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000000..ac0f908b9c Binary files /dev/null and b/core/ui/src/main/res/font/roboto_medium.ttf differ diff --git a/core/ui/src/main/res/font/roboto_regular.ttf b/core/ui/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000000..ddf4bfacb3 Binary files /dev/null and b/core/ui/src/main/res/font/roboto_regular.ttf differ diff --git a/core/ui/src/main/res/values-ru/string_common.xml b/core/ui/src/main/res/values-ru/string_common.xml new file mode 100644 index 0000000000..ebb5d6b913 --- /dev/null +++ b/core/ui/src/main/res/values-ru/string_common.xml @@ -0,0 +1,5 @@ + + + Поделиться + Копировать + \ No newline at end of file diff --git a/core/ui/src/main/res/values/dimens.xml b/core/ui/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..98fbe4b379 --- /dev/null +++ b/core/ui/src/main/res/values/dimens.xml @@ -0,0 +1,54 @@ + + + + 1dp + 2dp + 3dp + 4dp + 6dp + 8dp + 12dp + 16dp + 24dp + + + + 6dp + 12dp + 16dp + + + + 0dp + 4dp + 11dp + 16dp + 20dp + 24dp + 28dp + 32dp + 40dp + 48dp + 56dp + 84dp + 86dp + 102dp + 164dp + 200dp + + + + 2dp + 4dp + 8dp + 10dp + 12dp + 16dp + 24dp + 32dp + 44dp + 50dp + 54dp + 2dp + + \ No newline at end of file diff --git a/core/ui/src/main/res/values/string_common.xml b/core/ui/src/main/res/values/string_common.xml new file mode 100644 index 0000000000..ad2cf0d79f --- /dev/null +++ b/core/ui/src/main/res/values/string_common.xml @@ -0,0 +1,5 @@ + + + Share + Copy + \ No newline at end of file diff --git a/core/utils/.gitignore b/core/utils/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/utils/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts new file mode 100644 index 0000000000..917187555f --- /dev/null +++ b/core/utils/build.gradle.kts @@ -0,0 +1,35 @@ +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 + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) + + /** Coroutines */ + implementation(Library.coroutine) +} \ No newline at end of file diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..02f6bf24ad --- /dev/null +++ b/core/utils/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt new file mode 100644 index 0000000000..6af667ae08 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.converter + +interface Converter { + fun convert(value: I): O + fun convertList(input: List): List { + return input.map { convert(it) } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt new file mode 100644 index 0000000000..952424e215 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -0,0 +1,20 @@ +package com.tangem.utils.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +interface CoroutineDispatcherProvider { + val main: CoroutineDispatcher + val io: CoroutineDispatcher +} + +class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { + override val main: CoroutineDispatcher = Dispatchers.Main + override val io: CoroutineDispatcher = Dispatchers.IO +} + +class TestingCoroutineDispatcherProvider( + override val main: CoroutineDispatcher = Dispatchers.Unconfined, + override val io: CoroutineDispatcher = Dispatchers.Unconfined, +) : CoroutineDispatcherProvider \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt b/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt new file mode 100644 index 0000000000..df85354292 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt @@ -0,0 +1,16 @@ +package com.tangem.utils.di + +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface CoroutineDispatcherProviderModule { + + @Binds + fun bindCoroutineDispatcherProvider(coroutineDispatcherProvider: AppCoroutineDispatcherProvider): CoroutineDispatcherProvider +} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 6e1c48864e..6909db2791 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -75,9 +75,13 @@ dependencies { implementation(project(":common")) /** Tangem libraries */ - implementation(Tangem.blockchain) + implementation(Tangem.blockchain) { + exclude(module = "joda-time") + } implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) + implementation(Tangem.cardAndroid) { + exclude(module = "joda-time") + } /** Other libraries */ implementation(Library.reKotlin) diff --git a/features/referral/data/.gitignore b/features/referral/data/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts new file mode 100644 index 0000000000..497d0af645 --- /dev/null +++ b/features/referral/data/build.gradle.kts @@ -0,0 +1,40 @@ +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 + } +} + +dependencies { + + /** Project*/ + implementation(project(":core:datasource")) + implementation(project(":core:utils")) + implementation(project(":features:referral:domain")) + + /** Time */ + implementation(Library.jodatime) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/data/src/main/AndroidManifest.xml b/features/referral/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6f8fe3941b --- /dev/null +++ b/features/referral/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt new file mode 100644 index 0000000000..bbd136e5f2 --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -0,0 +1,57 @@ +package com.tangem.feature.referral.converters + +import com.tangem.datasource.api.referral.models.ReferralResponse +import com.tangem.datasource.api.referral.models.Token +import com.tangem.feature.referral.domain.models.DiscountType +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.feature.referral.domain.models.ReferralInfo +import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class ReferralConverter @Inject constructor() : Converter { + + override fun convert(value: ReferralResponse): ReferralData { + val conditions = value.conditions + val referral = value.referral + val tokenConverter = TokenConverter() + return if (referral != null) { + ReferralData.ParticipantData( + award = conditions.award, + discount = conditions.discount.amount, + discountType = DiscountType.valueOf(conditions.discount.discountType.name), + tosLink = conditions.tosLink, + tokens = tokenConverter.convertList(conditions.awards.map { it.token }), + referral = ReferralInfo( + shareLink = referral.shareLink, + address = referral.address, + promocode = referral.promocode, + walletsPurchased = referral.walletsPurchased, + termsAcceptedAt = referral.termsAcceptedAt, + ), + ) + } else { + ReferralData.NonParticipantData( + award = conditions.award, + discount = conditions.discount.amount, + discountType = DiscountType.valueOf(conditions.discount.discountType.name), + tosLink = conditions.tosLink, + tokens = tokenConverter.convertList(conditions.awards.map { it.token }), + ) + } + } +} + +private class TokenConverter : Converter { + + override fun convert(value: Token): TokenData { + return TokenData( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + ) + } +} \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt new file mode 100644 index 0000000000..789d5d3bc8 --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.referral.data + +import com.tangem.datasource.api.referral.ReferralApi +import com.tangem.datasource.api.referral.models.StartReferralBody +import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class ReferralRepositoryImpl( + private val referralApi: ReferralApi, + private val referralConverter: ReferralConverter, + private val coroutineDispatcher: CoroutineDispatcherProvider, +) : ReferralRepository { + + override suspend fun getReferralStatus(walletId: String): ReferralData { + return withContext(coroutineDispatcher.io) { + referralConverter.convert(referralApi.getReferralStatus(walletId)) + } + } + + override suspend fun startReferral( + walletId: String, + networkId: String, + tokenId: String, + address: String, + ): ReferralData { + return withContext(coroutineDispatcher.io) { + referralConverter.convert( + referralApi.startReferral( + StartReferralBody( + walletId = walletId, + networkId = networkId, + tokenId = tokenId, + address = address, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt new file mode 100644 index 0000000000..7a88c423cc --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.referral.di + +import com.tangem.datasource.api.referral.ReferralApi +import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.data.ReferralRepositoryImpl +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ReferralRepositoryModule { + + @Provides + @Singleton + fun provideReferralRepository( + referralApi: ReferralApi, + referralConverter: ReferralConverter, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): ReferralRepository { + return ReferralRepositoryImpl( + referralApi, referralConverter, coroutineDispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/features/referral/domain/.gitignore b/features/referral/domain/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts new file mode 100644 index 0000000000..bcee4c51cc --- /dev/null +++ b/features/referral/domain/build.gradle.kts @@ -0,0 +1,39 @@ +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 + } +} + +dependencies { + + /** Libs */ + implementation(project(":core:utils")) + implementation(project(":libs:crypto")) + + /** Time */ + implementation(Library.jodatime) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/domain/src/main/AndroidManifest.xml b/features/referral/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ace8d8e355 --- /dev/null +++ b/features/referral/domain/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt new file mode 100644 index 0000000000..a59b0e3b56 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.referral.domain + +import com.tangem.feature.referral.domain.models.ReferralData + +interface ReferralInteractor { + + suspend fun getReferralStatus(): ReferralData + + suspend fun startReferral(): ReferralData +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt new file mode 100644 index 0000000000..8b2716f539 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.referral.domain + +import com.tangem.feature.referral.domain.converter.TokensConverter +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.Currency + +internal class ReferralInteractorImpl( + private val repository: ReferralRepository, + private val derivationManager: DerivationManager, + private val userWalletManager: UserWalletManager, + private val tokensConverter: TokensConverter, +) : ReferralInteractor { + + private val tokensForReferral = mutableListOf() + + override suspend fun getReferralStatus(): ReferralData { + val refStatus = repository.getReferralStatus(userWalletManager.getWalletId()) + saveRefTokens(refStatus.tokens) + return refStatus + } + + override suspend fun startReferral(): ReferralData { + if (tokensForReferral.isNotEmpty()) { + val currency = tokensConverter.convert(tokensForReferral.first()) + deriveOrAddTokens(currency) + val publicAddress = userWalletManager.getWalletAddress(currency) + return repository.startReferral( + walletId = userWalletManager.getWalletId(), + networkId = currency.networkId, + tokenId = currency.id, + address = publicAddress, + ) + } else { + throw IllegalStateException("tokens for ref is empty") + } + } + + private suspend fun deriveOrAddTokens(currency: Currency) { + if (!derivationManager.hasDerivation(currency.networkId)) { + derivationManager.deriveMissingBlockchains(currency) + } + if (!userWalletManager.isTokenAdded(currency)) { + userWalletManager.addToken(currency) + } + } + + private fun saveRefTokens(tokens: List) { + tokensForReferral.clear() + tokensForReferral.addAll(tokens) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt new file mode 100644 index 0000000000..a329077747 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.referral.domain + +import com.tangem.feature.referral.domain.models.ReferralData + +interface ReferralRepository { + + /** Returns data object of [ReferralData] depends on user program status */ + suspend fun getReferralStatus(walletId: String): ReferralData + + /** Starts user referral program */ + suspend fun startReferral( + walletId: String, + networkId: String, + tokenId: String, + address: String, + ): ReferralData +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt new file mode 100644 index 0000000000..2f756567f1 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.referral.domain.converter + +import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.lib.crypto.models.Currency +import com.tangem.lib.crypto.models.NativeToken +import com.tangem.lib.crypto.models.NonNativeToken +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class TokensConverter @Inject constructor() : Converter { + + override fun convert(value: TokenData): Currency { + return if (value.decimalCount != null && + value.contractAddress != null + ) { + NonNativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + ) + } else { + NativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + ) + } + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt new file mode 100644 index 0000000000..0f0c22b0f0 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.referral.domain.di + +import com.tangem.feature.referral.domain.ReferralInteractor +import com.tangem.feature.referral.domain.ReferralInteractorImpl +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.feature.referral.domain.converter.TokensConverter +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ReferralDomainModule { + + @Provides + @Singleton + fun provideReferralInteractor( + referralRepository: ReferralRepository, + derivationManager: DerivationManager, + userWalletManager: UserWalletManager, + tokensConverter: TokensConverter, + ): ReferralInteractor { + return ReferralInteractorImpl( + referralRepository, derivationManager, userWalletManager, tokensConverter, + ) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt new file mode 100644 index 0000000000..f5cc061cb0 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.referral.domain.models + +import org.joda.time.DateTime + +sealed interface ReferralData { + + val award: Int + val discount: Int + val discountType: DiscountType + val tosLink: String + val tokens: List + + /** Data class that used if user is participant of program */ + data class ParticipantData( + override val award: Int, + override val discount: Int, + override val discountType: DiscountType, + override val tosLink: String, + override val tokens: List, + val referral: ReferralInfo, + ) : ReferralData + + /** Data class that used if user is not participant of program */ + data class NonParticipantData( + override val award: Int, + override val discount: Int, + override val discountType: DiscountType, + override val tosLink: String, + override val tokens: List, + ) : ReferralData +} + +data class TokenData( + val id: String, + val name: String, + val symbol: String, + val networkId: String, + val contractAddress: String?, + val decimalCount: Int?, +) + +data class ReferralInfo( + val shareLink: String, + val address: String, + val promocode: String, + val walletsPurchased: Int, + val termsAcceptedAt: DateTime?, +) + +enum class DiscountType { + PERCENTAGE, VALUE +} \ No newline at end of file diff --git a/features/referral/presentation/.gitignore b/features/referral/presentation/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts new file mode 100644 index 0000000000..b2763c706c --- /dev/null +++ b/features/referral/presentation/build.gradle.kts @@ -0,0 +1,56 @@ +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 + } +} + +dependencies { + /** Core modules */ + implementation(project(":core:ui")) + + /** AndroidX */ + implementation(AndroidX.appCompat) + + /** Compose */ + implementation(Compose.foundation) + implementation(Compose.material) + implementation(Compose.uiTooling) + + /** Domain */ + implementation(project(":features:referral:domain")) + + /** Other libraries */ + implementation(Library.composeShimmer) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/AndroidManifest.xml b/features/referral/presentation/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4c5a0a1f54 --- /dev/null +++ b/features/referral/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt new file mode 100644 index 0000000000..0f9a5666b3 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.referral.models + +data class ReferralStateHolder( + val headerState: HeaderState, + val referralInfoState: ReferralInfoState, + val effects: Effects, + val agreementBottomSheetState: AgreementBottomSheetState +) { + + data class HeaderState(val onBackClicked: () -> Unit) + + sealed interface ReferralInfoContentState + + sealed interface ReferralInfoState { + + data class ParticipantContent( + val award: String, + val address: String, + val discount: String, + val onParticipateClicked: () -> Unit, + ) : ReferralInfoState, ReferralInfoContentState + + data class NonParticipantContent( + val award: String, + val discount: String, + val purchasedWalletCount: Int, + val code: String, + val onCopyClicked: () -> Unit, + val onShareClicked: () -> Unit, + ) : ReferralInfoState, ReferralInfoContentState + + object Loading : ReferralInfoState + } + + data class Effects(val showErrorToast: Boolean) + + data class AgreementBottomSheetState(val url: String) +} diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt new file mode 100644 index 0000000000..e106965a14 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.referral.presentation + +import androidx.lifecycle.ViewModel +import com.tangem.feature.referral.domain.ReferralInteractor +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class ReferralViewModel @Inject constructor(referralInteractor: ReferralInteractor) : ViewModel() { + +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt new file mode 100644 index 0000000000..a1f8adc86c --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -0,0 +1,80 @@ +package com.tangem.feature.referral.ui + +import android.view.ViewGroup.LayoutParams +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.atoms.BottomSheetIndicator +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.presentation.R + +/** + * Bottom sheet content with referral program terms of use + * + * @param url link to the html page + */ +@Composable +fun AgreementBottomSheetContent(url: String) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colors.secondary) + .fillMaxWidth() + .height(LocalConfiguration.current.screenHeightDp.dp - dimensionResource(id = R.dimen.spacing16)), + ) { + BottomSheetIndicator() + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) + AgreementHtmlView(url = url) + } + } +} + +@Composable +private fun AgreementHtmlView(url: String) { + AndroidView( + modifier = Modifier.background(MaterialTheme.colors.secondary), + factory = { + WebView(it).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + webViewClient = WebViewClient() + settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } + loadUrl(url) + } + }, + update = { it.loadUrl(url) }, + ) +} + +@Preview +@Composable +fun Preview_AgreementBottomSheet_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AgreementBottomSheetContent(url = "https://tangem.com/en/") + } +} + +@Preview +@Composable +fun Preview_AgreementBottomSheet_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AgreementBottomSheetContent(url = "https://tangem.com/en/") + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt new file mode 100644 index 0000000000..133b6b374e --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -0,0 +1,77 @@ +package com.tangem.feature.referral.ui + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_AgreementText_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { + AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_AgreementText_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { + AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + } + } +} + +@Composable +fun AgreementText(@StringRes firstPartResId: Int, onClicked: () -> Unit) { + val agreementText = annotatedAgreementString(firstPart = stringResource(firstPartResId)) + ClickableText( + text = agreementText, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.spacing54)), + style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center), + maxLines = 2, + onClick = { + val clickableSpanStyle = requireNotNull(agreementText.spanStyles.getOrNull(1)) + if (it in clickableSpanStyle.start..clickableSpanStyle.end) { + onClicked() + } + }, + ) +} + +@Composable +private fun annotatedAgreementString(firstPart: String): AnnotatedString { + return buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY))) { + append("$firstPart ") + } + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.ACCENT))) { + append(stringResource(id = R.string.common_terms_and_conditions)) + } + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY))) { + append(" ${stringResource(id = R.string.referral_tou_suffix)}") + } + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt new file mode 100644 index 0000000000..59b7ab8148 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryStartIconButton +import com.tangem.core.ui.components.SmallInfoCard +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_NonParticipateBottomBlock_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + NonParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_NonParticipateBottomBlock_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + NonParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ) + } + } +} + +@Composable +fun NonParticipateBottomBlock( + purchasedWalletCount: Int, + code: String, + onCopyClicked: () -> Unit, + onShareClicked: () -> Unit, + onAgreementClicked: () -> Unit, +) { + Column( + modifier = Modifier + .padding( + top = dimensionResource(id = R.dimen.spacing24), + bottom = dimensionResource(id = R.dimen.spacing16), + ) + .padding(horizontal = dimensionResource(id = R.dimen.spacing16)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing16)), + ) { + SmallInfoCard( + startText = stringResource(id = R.string.referral_friends_bought_title), + endText = String.format( + stringResource(id = R.string.referral_wallets_purchased_count), + purchasedWalletCount, + ), + ) + PersonalCodeCard(code = code) + AdditionalButtons(onCopyClicked = onCopyClicked, onShareClicked = onShareClicked) + AgreementText(firstPartResId = R.string.referral_tou_enroled_prefix, onClicked = onAgreementClicked) + } +} + +@Composable +private fun PersonalCodeCard(code: String) { + Column( + modifier = Modifier + .shadow( + elevation = dimensionResource(id = R.dimen.elevation2), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + ) + .background( + color = MaterialTheme.colors.secondary, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + ) + .fillMaxWidth() + .padding(vertical = dimensionResource(id = R.dimen.spacing12)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing4)), + ) { + Text( + text = stringResource(id = R.string.referral_promo_code_title), + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + maxLines = 1, + style = MaterialTheme.typography.subtitle2, + ) + Text( + text = code, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.h2, + ) + } +} + +@Composable +private fun AdditionalButtons(onCopyClicked: () -> Unit, onShareClicked: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing16)), + ) { + PrimaryStartIconButton( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_copy), + iconResId = R.drawable.ic_copy, + onClicked = onCopyClicked, + ) + PrimaryStartIconButton( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share, + onClicked = onShareClicked, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt new file mode 100644 index 0000000000..7294cbd3f7 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryEndIconButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ParticipateBottomBlock_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ParticipateBottomBlock_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) + } + } +} + +@Composable +fun ParticipateBottomBlock(onAgreementClicked: () -> Unit, onParticipateClicked: () -> Unit) { + Column { + AgreementText( + firstPartResId = R.string.referral_tou_not_enroled_prefix, + onClicked = onAgreementClicked, + ) + PrimaryEndIconButton( + modifier = Modifier.padding(all = dimensionResource(id = R.dimen.spacing16)), + text = stringResource(id = R.string.referral_button_participate), + iconResId = R.drawable.ic_tangem, + onClicked = onParticipateClicked, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt new file mode 100644 index 0000000000..a9935a04cc --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -0,0 +1,453 @@ +package com.tangem.feature.referral.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.BottomSheetState +import androidx.compose.material.BottomSheetValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.VerticalSpacer +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.ButtonColorType +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.White +import com.tangem.core.ui.res.buttonColor +import com.tangem.core.ui.res.iconColor +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.models.ReferralStateHolder +import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState +import com.tangem.feature.referral.presentation.R +import com.valentinilk.shimmer.shimmer +import kotlinx.coroutines.launch + +/** + * Referral program screen for participant and non-participant + * + * @param stateHolder state holder + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ReferralScreen(stateHolder: ReferralStateHolder) { + val coroutineScope = rememberCoroutineScope() + val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), + ) + + TangemTheme { + BottomSheetScaffold( + sheetContent = { AgreementBottomSheetContent(stateHolder.agreementBottomSheetState.url) }, + scaffoldState = bottomSheetScaffoldState, + sheetShape = RoundedCornerShape( + topStart = dimensionResource(id = R.dimen.radius16), + topEnd = dimensionResource(id = R.dimen.radius16), + ), + sheetElevation = dimensionResource(id = R.dimen.elevation24), + sheetPeekHeight = dimensionResource(id = R.dimen.size0), + content = { + ReferralContent( + stateHolder = stateHolder, + onAgreementClicked = { + coroutineScope.launch { + if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) { + bottomSheetScaffoldState.bottomSheetState.expand() + } else { + bottomSheetScaffoldState.bottomSheetState.collapse() + } + } + }, + ) + }, + ) + } +} + +@Composable +fun ReferralContent(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colors.primary) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Header(stateHolder = stateHolder) + VerticalSpacer(spaceResId = R.dimen.spacing16) + ReferralInfo(stateHolder = stateHolder, onAgreementClicked = onAgreementClicked) + } +} + +@Composable +private fun ColumnScope.Header(stateHolder: ReferralStateHolder) { + AppBar(stateHolder = stateHolder) + Image( + painter = painterResource(R.drawable.ill_businessman_3d), + contentDescription = null, + modifier = Modifier + .padding(horizontal = dimensionResource(id = R.dimen.spacing32)) + .fillMaxWidth() + .height(dimensionResource(R.dimen.size200)), + ) + VerticalSpacer(spaceResId = R.dimen.spacing24) + Text( + text = stringResource(id = R.string.referral_title), + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing50)), + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + textAlign = TextAlign.Center, + maxLines = 2, + style = MaterialTheme.typography.h2, + ) +} + +@Composable +fun ReferralInfo(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) { + when (val state = stateHolder.referralInfoState) { + is ReferralInfoState.ParticipantContent -> { + Conditions(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing44) + ParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked) + } + is ReferralInfoState.NonParticipantContent -> { + Conditions(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing24) + NonParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked) + } + is ReferralInfoState.Loading -> { + LoadingCondition(iconResId = R.drawable.ic_tether) + VerticalSpacer(spaceResId = R.dimen.spacing32) + LoadingCondition(iconResId = R.drawable.ic_discount) + } + } +} + +@Composable +private fun AppBar(stateHolder: ReferralStateHolder) { + AppBarWithBackButton( + text = stringResource(R.string.details_referral_title), + onBackClick = stateHolder.headerState.onBackClicked, + ) +} + +@Composable +private fun Conditions(state: ReferralStateHolder.ReferralInfoContentState) { + ConditionForYou(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing32) + ConditionForYourFriend(state = state) +} + +@Composable +private fun ConditionForYou(state: ReferralStateHolder.ReferralInfoContentState) { + Condition(iconResId = R.drawable.ic_tether) { + when (state) { + is ReferralInfoState.ParticipantContent -> InfoForYou(award = state.award, address = state.address) + is ReferralInfoState.NonParticipantContent -> InfoForYou(award = state.award) + } + } +} + +@Composable +private fun ConditionForYourFriend(state: ReferralStateHolder.ReferralInfoContentState) { + Condition(iconResId = R.drawable.ic_discount) { + when (state) { + is ReferralInfoState.ParticipantContent -> InfoForYourFriend(discount = state.discount) + is ReferralInfoState.NonParticipantContent -> InfoForYourFriend(discount = state.discount) + } + } +} + +@Composable +private fun LoadingCondition(@DrawableRes iconResId: Int) { + Condition(iconResId = iconResId) { + ShimmerInfo() + } +} + +@Composable +private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> Unit) { + Row( + modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.spacing16)), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing12)), + verticalAlignment = Alignment.Top, + ) { + Box( + modifier = Modifier + .background( + color = MaterialTheme.colors.buttonColor(type = ButtonColorType.SECONDARY), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius16)), + ) + .size(dimensionResource(id = R.dimen.size56)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size28)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + infoBlock() + } +} + +@Composable +private fun InfoForYou(award: String, address: String? = null) { + ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { + Text( + text = buildAnnotatedString { + append(stringResource(id = R.string.referral_point_currencies_description_prefix)) + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1))) { + append(" $award ") + } + append( + String.format( + stringResource(id = R.string.referral_point_currencies_description_suffix), + if (!address.isNullOrBlank()) address else "", + ), + ) + }, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + style = MaterialTheme.typography.body2, + ) + } +} + +@Composable +private fun InfoForYourFriend(discount: String) { + ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { + Text( + text = buildString { + append(stringResource(id = R.string.referral_point_discount_description_prefix)) + append( + String.format( + stringResource(id = R.string.referral_point_discount_description_value), + " $discount", + ), + ) + append(" ") + append(stringResource(id = R.string.referral_point_discount_description_suffix)) + }, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + style = MaterialTheme.typography.body2, + ) + } +} + +@Composable +private fun ConditionInfo(title: String, subtitleContent: @Composable () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing2))) { + Text( + text = title, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + style = MaterialTheme.typography.subtitle1, + ) + subtitleContent() + } +} + +@Composable +private fun ShimmerInfo() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = dimensionResource(id = R.dimen.spacing4)) + .shimmer(), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing10)), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) + .width(dimensionResource(id = R.dimen.size102)) + .height(dimensionResource(id = R.dimen.size16)) + .background(White), + ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) + .width(dimensionResource(id = R.dimen.size40)) + .height(dimensionResource(id = R.dimen.size11)) + .background(White), + ) + } +} + +@Composable +private fun ParticipantBottomBlock(state: ReferralInfoState.ParticipantContent, onAgreementClicked: () -> Unit) { + ParticipateBottomBlock( + onAgreementClicked = onAgreementClicked, + onParticipateClicked = state.onParticipateClicked, + ) +} + +@Composable +private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantContent, onAgreementClicked: () -> Unit) { + NonParticipateBottomBlock( + purchasedWalletCount = state.purchasedWalletCount, + code = state.code, + onCopyClicked = state.onCopyClicked, + onShareClicked = state.onShareClicked, + onAgreementClicked = onAgreementClicked, + ) +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + diff --git a/features/referral/presentation/src/main/res/values-ru/string.xml b/features/referral/presentation/src/main/res/values-ru/string.xml new file mode 100644 index 0000000000..4a6e997f8e --- /dev/null +++ b/features/referral/presentation/src/main/res/values-ru/string.xml @@ -0,0 +1,20 @@ + + + Реферальная программа + Приведи друга в Tangem + Вы + Получите + за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %s + Ваш друг + Получит + %s скидку + при покупке карточки на сайте tangem.com + Нажимая на эту кнопку вы принимаете + условия участия + в реферальной программе + Участвовать + Вы приняли + Ваш персональный код + %d кошельков + Ваши друзья купили + \ No newline at end of file diff --git a/features/referral/presentation/src/main/res/values/string.xml b/features/referral/presentation/src/main/res/values/string.xml new file mode 100644 index 0000000000..7042ed2411 --- /dev/null +++ b/features/referral/presentation/src/main/res/values/string.xml @@ -0,0 +1,20 @@ + + + Referral program + Refer your friends to Tangem + You + Will get + for each wallet bought by your friend on your Tron network address %s + Your friend + Will get a + %s discount + when buying a card on tangem.com + By tapping this button you accept + terms and conditions + of the referral program + Participate + You\'ve accepted + Your personal code + %d wallets + Your friends bought + \ No newline at end of file diff --git a/libs/crypto/.gitignore b/libs/crypto/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libs/crypto/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts new file mode 100644 index 0000000000..b6d1313ae0 --- /dev/null +++ b/libs/crypto/build.gradle.kts @@ -0,0 +1,29 @@ +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 + } +} + +dependencies { + + /** Coroutines */ + implementation(Library.coroutine) +} \ No newline at end of file diff --git a/libs/crypto/src/main/AndroidManifest.xml b/libs/crypto/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1fd7238d41 --- /dev/null +++ b/libs/crypto/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt new file mode 100644 index 0000000000..b9b2cfecea --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -0,0 +1,18 @@ +package com.tangem.lib.crypto + +import com.tangem.lib.crypto.models.Currency + +interface DerivationManager { + + /** + * Derives missing blockchain and returns flag true if did it successfully + * + * @param currency to derive (Native token or not) + */ + suspend fun deriveMissingBlockchains(currency: Currency): Boolean + + /** + * Checks that given [networkId] has derivations + */ + fun hasDerivation(networkId: String): Boolean +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt new file mode 100644 index 0000000000..6df9887170 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -0,0 +1,40 @@ +package com.tangem.lib.crypto + +import com.tangem.lib.crypto.models.Currency + +/** + * Provider for user tokens data + */ +interface UserWalletManager { + + /** + * Returns all user tokens (merged from local and backend) + */ + suspend fun getUserTokens(): List + + /** + * Returns user walletId + */ + fun getWalletId(): String + + /** + * Checks that token added to user wallet + * + * @param currency to receive referral payments + */ + suspend fun isTokenAdded(currency: Currency): Boolean + + /** + * Adds token to wallet if its not + * + * @param currency to add to wallet + */ + fun addToken(currency: Currency) + + /** + * Returns wallet public address for token + * + * @param currency for which find address + */ + fun getWalletAddress(currency: Currency): String +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt new file mode 100644 index 0000000000..82afac8e25 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt @@ -0,0 +1,12 @@ +package com.tangem.lib.crypto.models + +/** + * Currency data class that can be native blockchain Token + * or custom Token (used in this lib to replace and divide logic Currency from app module) + */ +abstract class Currency( + open val id: String, + open val name: String, + open val symbol: String, + open val networkId: String, +) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt new file mode 100644 index 0000000000..f8ab7464d6 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt @@ -0,0 +1,8 @@ +package com.tangem.lib.crypto.models + +data class NativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, +) : Currency(id, name, symbol, networkId) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt new file mode 100644 index 0000000000..a2d812cd1b --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt @@ -0,0 +1,10 @@ +package com.tangem.lib.crypto.models + +class NonNativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, + val contractAddress: String, + val decimalCount: Int, +) : Currency(id, name, symbol, networkId) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5e820bbbfc..e6a3474eac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,20 @@ include(":app") include(":domain") include(":network") -include(":common") \ No newline at end of file +include(":common") + +// region Core modules +include(":core:datasource") +include(":core:utils") +include(":core:ui") +// endregion Core modules + +// region Libs modules +include(":libs:crypto") +// endregion Libs modules + +// region Feature modules +include(":features:referral:data") +include(":features:referral:domain") +include(":features:referral:presentation") +// endregion Feature modules \ No newline at end of file