Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-21 15:48:51 +03:00
commit 44837422c7
94 changed files with 3076 additions and 29 deletions

View file

@ -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)

View file

@ -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

View file

@ -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() {

View file

@ -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,

View file

@ -184,7 +184,7 @@ class TokensMiddleware {
}
}
private fun deriveMissingBlockchains(
fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: (ScanResponse) -> Unit,

View file

@ -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<AppState>? = null
val tokesMiddleware: TokensMiddleware? = null
fun getActualCard(): Card? {
return scanResponse?.card
}
}

View file

@ -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<Boolean> { 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
}
}

View file

@ -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<Currency> {
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
}
}

View file

@ -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,
)
}
}

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,8C13.1,8 14,7.1 14,6C14,4.9 13.1,4 12,4C10.9,4 10,4.9 10,6C10,7.1 10.9,8 12,8ZM12,10C10.9,10 10,10.9 10,12C10,13.1 10.9,14 12,14C13.1,14 14,13.1 14,12C14,10.9 13.1,10 12,10ZM12,16C10.9,16 10,16.9 10,18C10,19.1 10.9,20 12,20C13.1,20 14,19.1 14,18C14,16.9 13.1,16 12,16Z"
android:fillColor="#000"
android:fillType="evenOdd" />
</vector>

View file

@ -2,9 +2,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/details_menu"
android:title="@string/details_title"
android:icon="@drawable/ic_more_vertical_24"
app:iconTint="@color/darkGray6"
app:showAsAction="ifRoom" />
android:id="@+id/details_menu"
android:title="@string/details_title"
android:icon="@drawable/ic_more_vertical"
app:iconTint="@color/darkGray6"
app:showAsAction="ifRoom" />
</menu>

View file

@ -14,6 +14,10 @@ buildscript {
}
}
plugins {
id("com.google.dagger.hilt.android") version "2.44" apply false
}
allprojects {
repositories {
google()

View file

@ -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

View file

@ -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"

1
core/datasource/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.datasource" />

View file

@ -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
}

View file

@ -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<Award>,
@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
}

View file

@ -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,
)

View file

@ -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 = ""
}
}

1
core/ui/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

41
core/ui/build.gradle.kts Normal file
View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.ui" />

View file

@ -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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=233%3A258&t=WdN5XpixzZLlQAZO-4"
* >Figma component</a>
*/
@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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=68%3A47&t=WdN5XpixzZLlQAZO-4"
* >Figma component</a>
*/
@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,
)
}

View file

@ -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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A339&t=WdN5XpixzZLlQAZO-4"
* >Figma component</a>
*/
@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
)
}
}
}

View file

@ -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)))
}

View file

@ -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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=50%3A403&t=tRQ4KlzkjV7TCLZl-4">
* Figma Component</a>
*/
@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 = {},
),
)
}
}

View file

@ -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 <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=123%3A287&t=WdN5XpixzZLlQAZO-4"
* >Figma component</a>
*/
@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,
)
}
}
}

View file

@ -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,
)

View file

@ -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 <a href="https://www.figma.com/file/17JyRbuUEZ42DluaFEuGQk/Atoms?node-id=135%3A25&t=3dvxYMZBox4jxJc1-4">
* Figma Component</a>
*/
@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()
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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),
),
)

View file

@ -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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,21H8V7H19V21ZM19,5H8C7.47,5 6.961,5.211 6.586,5.586C6.211,5.961 6,6.47 6,7V21C6,21.53 6.211,22.039 6.586,22.414C6.961,22.789 7.47,23 8,23H19C19.53,23 20.039,22.789 20.414,22.414C20.789,22.039 21,21.53 21,21V7C21,6.47 20.789,5.961 20.414,5.586C20.039,5.211 19.53,5 19,5ZM16,1H4C3.47,1 2.961,1.211 2.586,1.586C2.211,1.961 2,2.47 2,3V17H4V3H16V1Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M22.04,10.71V6H17.34L14,2.69L10.71,6H6V10.71L2.69,14L6,17.34V22.04H10.71L14,25.35L17.34,22.04H22.04V17.34L25.35,14L22.04,10.71ZM10.83,9.05C11.81,9.05 12.6,9.84 12.6,10.83C12.6,11.299 12.413,11.75 12.082,12.082C11.75,12.413 11.299,12.6 10.83,12.6C9.84,12.6 9.05,11.81 9.05,10.83C9.05,9.84 9.84,9.05 10.83,9.05ZM17.22,19C16.24,19 15.45,18.2 15.45,17.22C15.45,16.751 15.637,16.3 15.968,15.968C16.3,15.637 16.751,15.45 17.22,15.45C18.2,15.45 19,16.24 19,17.22C19,17.692 18.813,18.145 18.479,18.479C18.145,18.813 17.692,19 17.22,19ZM10.5,19.03L9,17.53L17.53,9L19.03,10.5L10.5,19.03Z"
android:fillColor="#000000" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,8C13.1,8 14,7.1 14,6C14,4.9 13.1,4 12,4C10.9,4 10,4.9 10,6C10,7.1 10.9,8 12,8ZM12,10C10.9,10 10,10.9 10,12C10,13.1 10.9,14 12,14C13.1,14 14,13.1 14,12C14,10.9 13.1,10 12,10ZM12,16C10.9,16 10,16.9 10,18C10,19.1 10.9,20 12,20C13.1,20 14,19.1 14,18C14,16.9 13.1,16 12,16Z"
android:fillColor="#000"
android:fillType="evenOdd" />
</vector>

View file

@ -2,7 +2,7 @@
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h24v24h-24z"/>
<clip-path android:pathData="M0,0h24v24h-24z" />
<path android:fillColor="#000000" android:pathData="M2,4H6V2H2C1.47,2 0.961,2.211 0.586,2.586C0.211,2.961 0,3.47 0,4L0,8H2V4ZM22,2H18V4H22V8H24V4C24,3.47 23.789,2.961 23.414,2.586C23.039,2.211 22.53,2 22,2ZM2,16H0V20C0,20.53 0.211,21.039 0.586,21.414C0.961,21.789 1.47,22 2,22H6V20H2V16ZM22,20H18V22H22C22.53,22 23.039,21.789 23.414,21.414C23.789,21.039 24,20.53 24,20V16H22V20ZM4,8V16C4,16.53 4.211,17.039 4.586,17.414C4.961,17.789 5.47,18 6,18H18C18.53,18 19.039,17.789 19.414,17.414C19.789,17.039 20,16.53 20,16V8C20,7.47 19.789,6.961 19.414,6.586C19.039,6.211 18.53,6 18,6H6C5.47,6 4.961,6.211 4.586,6.586C4.211,6.961 4,7.47 4,8ZM6,16V12H18V16H6ZM18,8V10H6V8H18Z"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8C18.796,8 19.559,7.684 20.121,7.121C20.684,6.559 21,5.796 21,5C21,4.204 20.684,3.441 20.121,2.879C19.559,2.316 18.796,2 18,2C17.204,2 16.441,2.316 15.879,2.879C15.316,3.441 15,4.204 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9C5.204,9 4.441,9.316 3.879,9.879C3.316,10.441 3,11.204 3,12C3,12.796 3.316,13.559 3.879,14.121C4.441,14.684 5.204,15 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,18.226 20.612,17.483 20.065,16.935C19.517,16.388 18.774,16.08 18,16.08Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<group>
<clip-path android:pathData="M0,0h28v28h-28z" />
<path
android:pathData="M8.821,17.869C9.035,16.016 9.741,14.312 10.803,12.892C10.407,13.076 9.929,13.195 9.38,13.241V14H8.552V13.247C6.83,13.134 5.744,12.22 5.738,10.863H7.015C7.077,11.556 7.657,12.04 8.552,12.142V9.483L8.015,9.346C6.67,9.017 5.935,8.234 5.935,7.093C5.935,5.749 6.929,4.852 8.552,4.709V3.902H9.38V4.709C10.941,4.84 11.966,5.766 12.003,7.063H10.744C10.719,6.424 10.176,5.922 9.38,5.826V8.342L9.941,8.467C11.454,8.82 12.163,9.561 12.163,10.768C12.163,11.028 12.13,11.27 12.065,11.493C13.628,10.047 15.64,9.079 17.869,8.822C17.808,3.939 13.831,0 8.935,0C4,0 0,4 0,8.935C0,13.831 3.939,17.808 8.821,17.869ZM17.776,10.222C13.873,10.786 10.786,13.873 10.222,17.776L17.776,10.222ZM8.552,8.174C7.688,8.007 7.225,7.589 7.225,6.991C7.225,6.352 7.781,5.862 8.552,5.82V8.174ZM9.38,12.154V9.632C10.404,9.83 10.88,10.236 10.88,10.905C10.88,11.646 10.33,12.1 9.38,12.154Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
<path
android:pathData="M10.13,19.066C10.13,14.132 14.13,10.131 19.064,10.131C23.998,10.131 27.999,14.132 27.999,19.066C27.999,24 23.998,28.001 19.064,28.001C14.13,28.001 10.13,24 10.13,19.066ZM19.527,17.526L20.48,14.823L19.066,14.323L17.782,17.962L15.337,18.573L15.701,20.028L17.183,19.658L16.23,22.36C16.149,22.589 16.184,22.844 16.325,23.043C16.465,23.241 16.694,23.36 16.937,23.36H22.61V21.86H17.997L18.928,19.222L21.374,18.61L21.01,17.155L19.527,17.526Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="common_share">Поделиться</string>
<string name="common_copy">Копировать</string>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- region Elevation -->
<dimen name="elevation1">1dp</dimen>
<dimen name="elevation2">2dp</dimen>
<dimen name="elevation3">3dp</dimen>
<dimen name="elevation4">4dp</dimen>
<dimen name="elevation6">6dp</dimen>
<dimen name="elevation8">8dp</dimen>
<dimen name="elevation12">12dp</dimen>
<dimen name="elevation16">16dp</dimen>
<dimen name="elevation24">24dp</dimen>
<!-- endregion Elevation -->
<!-- region Radius -->
<dimen name="radius6">6dp</dimen>
<dimen name="radius12">12dp</dimen>
<dimen name="radius16">16dp</dimen>
<!-- endregion Radius -->
<!-- region Size -->
<dimen name="size0">0dp</dimen>
<dimen name="size4">4dp</dimen>
<dimen name="size11">11dp</dimen>
<dimen name="size16">16dp</dimen>
<dimen name="size20">20dp</dimen>
<dimen name="size24">24dp</dimen>
<dimen name="size28">28dp</dimen>
<dimen name="size32">32dp</dimen>
<dimen name="size40">40dp</dimen>
<dimen name="size48">48dp</dimen>
<dimen name="size56">56dp</dimen>
<dimen name="size84">84dp</dimen>
<dimen name="size86">86dp</dimen>
<dimen name="size102">102dp</dimen>
<dimen name="size164">164dp</dimen>
<dimen name="size200">200dp</dimen>
<!-- endregion Size -->
<!-- region Spacing -->
<dimen name="spacing2">2dp</dimen>
<dimen name="spacing4">4dp</dimen>
<dimen name="spacing8">8dp</dimen>
<dimen name="spacing10">10dp</dimen>
<dimen name="spacing12">12dp</dimen>
<dimen name="spacing16">16dp</dimen>
<dimen name="spacing24">24dp</dimen>
<dimen name="spacing32">32dp</dimen>
<dimen name="spacing44">44dp</dimen>
<dimen name="spacing50">50dp</dimen>
<dimen name="spacing54">54dp</dimen>
<dimen name="radius2">2dp</dimen>
<!-- endregion Spacing -->
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="common_share">Share</string>
<string name="common_copy">Copy</string>
</resources>

1
core/utils/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.utils" />

View file

@ -0,0 +1,8 @@
package com.tangem.utils.converter
interface Converter<I, O> {
fun convert(value: I): O
fun convertList(input: List<I>): List<O> {
return input.map { convert(it) }
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

1
features/referral/data/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.referral.data" />

View file

@ -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<ReferralResponse, ReferralData> {
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<Token, TokenData> {
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,
)
}
}

View file

@ -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,
),
),
)
}
}
}

View file

@ -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,
)
}
}

1
features/referral/domain/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.referral.domain" />

View file

@ -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
}

View file

@ -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<TokenData>()
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<TokenData>) {
tokensForReferral.clear()
tokensForReferral.addAll(tokens)
}
}

View file

@ -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
}

View file

@ -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<TokenData, Currency> {
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,
)
}
}
}

View file

@ -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,
)
}
}

View file

@ -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<TokenData>
/** 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<TokenData>,
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<TokenData>,
) : 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
}

View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.referral.presentation" />

View file

@ -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)
}

View file

@ -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() {
}

View file

@ -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/")
}
}

View file

@ -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)}")
}
}
}

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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 = "",
),
),
)
}
}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="details_referral_title">Реферальная программа</string>
<string name="referral_title">Приведи друга в Tangem</string>
<string name="referral_point_currencies_title">Вы</string>
<string name="referral_point_currencies_description_prefix">Получите</string>
<string name="referral_point_currencies_description_suffix">за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %s</string>
<string name="referral_point_discount_title">Ваш друг</string>
<string name="referral_point_discount_description_prefix">Получит</string>
<string name="referral_point_discount_description_value">%s скидку</string>
<string name="referral_point_discount_description_suffix">при покупке карточки на сайте tangem.com</string>
<string name="referral_tou_not_enroled_prefix">Нажимая на эту кнопку вы принимаете</string>
<string name="common_terms_and_conditions">условия участия</string>
<string name="referral_tou_suffix">в реферальной программе</string>
<string name="referral_button_participate">Участвовать</string>
<string name="referral_tou_enroled_prefix">Вы приняли</string>
<string name="referral_promo_code_title">Ваш персональный код</string>
<string name="referral_wallets_purchased_count">%d кошельков</string>
<string name="referral_friends_bought_title">Ваши друзья купили</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="details_referral_title">Referral program</string>
<string name="referral_title">Refer your friends to Tangem</string>
<string name="referral_point_currencies_title">You</string>
<string name="referral_point_currencies_description_prefix">Will get</string>
<string name="referral_point_currencies_description_suffix">for each wallet bought by your friend on your Tron network address %s</string>
<string name="referral_point_discount_title">Your friend</string>
<string name="referral_point_discount_description_prefix">Will get a</string>
<string name="referral_point_discount_description_value">%s discount</string>
<string name="referral_point_discount_description_suffix">when buying a card on tangem.com</string>
<string name="referral_tou_not_enroled_prefix">By tapping this button you accept</string>
<string name="common_terms_and_conditions">terms and conditions</string>
<string name="referral_tou_suffix">of the referral program</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_tou_enroled_prefix">You\'ve accepted</string>
<string name="referral_promo_code_title">Your personal code</string>
<string name="referral_wallets_purchased_count">%d wallets</string>
<string name="referral_friends_bought_title">Your friends bought</string>
</resources>

1
libs/crypto/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.lib.crypto" />

View file

@ -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
}

View file

@ -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<Currency>
/**
* 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
}

View file

@ -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,
)

View file

@ -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)

View file

@ -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)

View file

@ -1,4 +1,20 @@
include(":app")
include(":domain")
include(":network")
include(":common")
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