Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-21 11:08:25 +03:00
commit f0a85e8b5b
508 changed files with 12810 additions and 8013 deletions

View file

@ -0,0 +1,138 @@
package com.tangem.core.analytics.models
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
companion object
}
sealed class RateApp(val value: String) {
object Liked : RateApp("Liked")
object Disliked : RateApp("Disliked")
object Closed : RateApp("Close")
}
sealed class OnOffState(val value: String) {
object On : OnOffState("On")
object Off : OnOffState("Off")
}
sealed class OrganizeSortType(val value: String) {
object ByBalance : OrganizeSortType("By Balance")
object Manually : OrganizeSortType("Manually")
}
sealed class UserCode(val value: String) {
object AccessCode : UserCode("Access Code")
object Passcode : UserCode("Passcode")
}
sealed class AccessCodeRecoveryStatus(val value: String) {
val key: String = "Status"
object Enabled : AccessCodeRecoveryStatus("Enabled")
object Disabled : AccessCodeRecoveryStatus("Disabled")
companion object {
fun from(enabled: Boolean): AccessCodeRecoveryStatus {
return if (enabled) Enabled else Disabled
}
}
}
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScannedFrom(val value: String) {
object Introduction : ScannedFrom("Introduction")
object Main : ScannedFrom("Main")
object SignIn : ScannedFrom("Sign In")
object MyWallets : ScannedFrom("My Wallets")
}
sealed class TxSentFrom(val value: String) {
data class Send(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Swap"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
}
sealed interface TxData {
val blockchain: String
val token: String
val feeType: FeeType
}
sealed class FeeType(val value: String) {
object Fixed : FeeType("Fixed")
object Min : FeeType("Min")
object Normal : FeeType("Normal")
object Max : FeeType("Max")
companion object {
fun fromString(feeType: String): FeeType {
return when (feeType) {
Min.value -> Min
Normal.value -> Normal
Max.value -> Max
Fixed.value -> Fixed
else -> Fixed
}
}
}
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
object SeedImport : WalletCreationType("Seed import")
}
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token"
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val CURRENCY = "Currency"
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"
const val ERROR_KEY = "Error Key"
const val CREATION_TYPE = "Creation type"
const val DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url"
const val METHOD_NAME = "Method Name"
const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"
const val BLOCKCHAIN_SELECTED_HOST = "selected_host"
}
}

View file

@ -11,8 +11,10 @@ dependencies {
/** Project */
implementation(projects.core.utils)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding.models)
/** Tangem libraries */
implementation(deps.tangem.blockchain)

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
class LocalDateAdapter : JsonAdapter<LocalDate>() {
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
@FromJson
override fun fromJson(reader: JsonReader): LocalDate? {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
}
@ToJson
override fun toJson(writer: JsonWriter, value: LocalDate?) {
if (value != null) {
writer.value(formatter.print(value))
} else {
writer.nullValue()
}
}
}

View file

@ -1,14 +1,12 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import org.joda.time.LocalDate
/**
* 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?,
@Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?,
) {
data class Conditions(
@ -45,4 +43,16 @@ data class ReferralResponse(
@Json(name = "walletsPurchased") val walletsPurchased: Int,
@Json(name = "termsAcceptedAt") val termsAcceptedAt: String?,
)
data class ExpectedAwards(
@Json(name = "numberOfWallets") val numberOfWallets: Int,
@Json(name = "list") val list: List<AwardItem>,
) {
data class AwardItem(
@Json(name = "currency") val currency: String,
@Json(name = "paymentDate") val paymentDate: LocalDate,
@Json(name = "amount") val amount: Int,
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.datasource.local.apptheme.DefaultAppThemeModeStore
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object AppThemeModeDataModule {
@Provides
fun provideAppThemeModeStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): AppThemeModeStore {
return DefaultAppThemeModeStore(
dataStore = SharedPreferencesDataStore(
preferencesName = "app_theme",
context = context,
adapter = moshi.adapter(AppThemeMode::class.java),
),
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.appcurrency.implementation.BalanceStateHidingSettingsStore
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object HiddenBalanceDataModule {
@Provides
fun provideHiddenBalanceStateStore(
@ApplicationContext context: Context,
@NetworkMoshi moshi: Moshi,
): BalanceHidingSettingsStore {
return BalanceStateHidingSettingsStore(
dataStore = SharedPreferencesDataStore(
preferencesName = "balance_hiding_settings",
context = context,
adapter = moshi.adapter(BalanceHidingSettings::class.java),
),
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object MarketCoinsStoreModule {
@Provides
@Singleton
fun provideUserMarketCoinsStore(): UserMarketCoinsStore {
return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,6 +21,7 @@ class MoshiModule {
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}

View file

@ -8,7 +8,6 @@ import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.allowLogging
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -76,7 +75,7 @@ class NetworkModule {
private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL)
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
.create(PromotionApi::class.java)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import kotlinx.coroutines.flow.Flow
interface BalanceHidingSettingsStore {
fun get(): Flow<BalanceHidingSettings>
suspend fun getSyncOrDefault(): BalanceHidingSettings
suspend fun store(settings: BalanceHidingSettings)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
internal class BalanceStateHidingSettingsStore(
dataStore: StringKeyDataStore<BalanceHidingSettings>,
) : BalanceHidingSettingsStore, KeylessDataStoreDecorator<BalanceHidingSettings>(dataStore) {
override suspend fun getSyncOrDefault(): BalanceHidingSettings {
return getSyncOrNull() ?: BalanceHidingSettings(
isHidingEnabledInSettings = false,
isBalanceHidden = false,
)
}
}

View file

@ -7,8 +7,4 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore) {
override suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}
}
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import kotlinx.coroutines.flow.Flow
interface AppThemeModeStore {
fun get(): Flow<AppThemeMode>
suspend fun store(item: AppThemeMode)
suspend fun isEmpty(): Boolean
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.apptheme
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
internal class DefaultAppThemeModeStore(
dataStore: StringKeyDataStore<AppThemeMode>,
) : AppThemeModeStore, KeylessDataStoreDecorator<AppThemeMode>(dataStore)

View file

@ -22,6 +22,10 @@ internal abstract class KeylessDataStoreDecorator<Value : Any>(
store(Unit, item)
}
open suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}
private companion object {
const val STRING_KEY = "key"
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
internal class DefaultUserMarketCoinsStore(
private val dataStore: StringKeyDataStore<CoinsResponse>,
) : UserMarketCoinsStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) {
dataStore.store(userWalletId.stringValue, item)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.wallets.models.UserWalletId
interface UserMarketCoinsStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse?
suspend fun store(userWalletId: UserWalletId, item: CoinsResponse)
}

View file

@ -3,10 +3,6 @@
"name": "OPTIMISM_SWAP_FEATURE_ENABLED",
"version": "4.3.1"
},
{
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
"version": "4.7.0"
},
{
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
@ -26,5 +22,13 @@
{
"name": "SHOPIFY_DYNAMIC_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED",
"version": "undefined"
},
{
"name": "DARK_THEME_ENABLED",
"version": "undefined"
}
]

View file

@ -35,4 +35,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) {
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),
AppCurrencySelector,
}

View file

@ -10,5 +10,7 @@ interface ReduxNavController {
/** Navigate by [action] */
fun navigate(action: NavigationAction)
fun popBackStack(screen: AppScreen? = null)
fun getBackStack(): List<AppScreen>
}

View file

@ -28,11 +28,16 @@
<string name="app_settings_saved_access_codes_footer">Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация.</string>
<string name="app_settings_saved_wallet">Cохранение кошелька</string>
<string name="app_settings_saved_wallet_footer">Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту.</string>
<string name="app_settings_theme_mode_dark">Тёмная</string>
<string name="app_settings_theme_mode_light">Светлая</string>
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="button_start_backup_process">Начать резервное копирование</string>
<plurals name="card_label_card_count">
<item quantity="one">%d карта</item>
<item quantity="few">%d карты</item>
@ -43,6 +48,7 @@
<string name="card_settings_access_code_recovery_enabled_description">Использовать эту карту для сброса кода доступа на других картах в этом кошельке</string>
<string name="card_settings_access_code_recovery_footer">Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька</string>
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_action_sheet_reset">Сбросить</string>
<string name="card_settings_action_sheet_title">Вы уверены, что хотите это сделать?</string>
<string name="card_settings_change_access_code">Смена кода доступа</string>
<string name="card_settings_change_access_code_footer">Код доступа будет изменен только на данной карте</string>
@ -65,6 +71,7 @@
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
<string name="common_biometrics">биометрией</string>
<string name="common_buy">Купить</string>
<string name="common_buy_currency">Купить %1$s</string>
<string name="common_camera_denied_alert_message">Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности.</string>
<string name="common_cancel">Отмена</string>
<string name="common_close">Закрыть</string>
@ -96,7 +103,6 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_reset">Сбросить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -187,6 +193,7 @@
<string name="initial_message_tap_header">Приложите карту</string>
<string name="internal_error_wallet_manager_not_found">Внутренняя ошибка: не удается найти менеджер кошельков</string>
<string name="key_invalidated_warning_description">Вы обновили данные биометрии, отсканируйте свою карту для входа</string>
<string name="main_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string>
<string name="main_get_bonus_subtitle">Вы успешно прошли все уроки и теперь можете получить 1INCH токены</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Пройдите 3 урока и получите %d 1INCH токен на свой кошелек</item>
@ -196,7 +203,7 @@
</plurals>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>
<string name="main_no_backup_warning_title">Резервное копирование не выполнено</string>
<string name="main_page_balance">Баланс</string>
<string name="main_processing_full_amount">В сумме учтены не все монеты</string>
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</string>
@ -210,6 +217,9 @@
<item quantity="other">Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту</item>
</plurals>
<string name="main_warning_missing_derivation_title">Некоторые адреса отсутствуют</string>
<string name="manage_tokens_add">Добавить</string>
<string name="manage_tokens_edit">Изменить</string>
<string name="notification_title_not_enough_funds">Невозможно покрыть %1$s комиссию</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Позже вы сможете установить индивидуальный код доступа для каждой карты</string>
@ -407,7 +417,7 @@
<string name="story_meet_pay">Расплачивайтесь</string>
<string name="story_meet_send">Отправляйте</string>
<string name="story_meet_store">Храните</string>
<string name="story_meet_title">Встречайте\nTangem</string>
<string name="story_meet_title">Встречайте Tangem</string>
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
<string name="story_web3_title">Поддержка DeFi</string>
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
@ -487,7 +497,8 @@
<string name="user_wallet_list_rename_popup_title">Переименование кошелька</string>
<string name="user_wallet_list_single_header">Одновалютные</string>
<string name="user_wallet_list_title">Мои кошельки</string>
<string name="user_wallet_list_unlock_all">Разблокировать все с %s</string>
<string name="user_wallet_list_unlock_all">Разблокировать все</string>
<string name="user_wallet_list_unlock_all_with">Разблокировать все с %s</string>
<string name="wallet_address_button_explore">История транзакций</string>
<string name="wallet_balance_blockchain_unreachable">Сеть недоступна</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуйте позже.</string>
@ -573,5 +584,5 @@
<string name="welcome_unlock">Войти с %s</string>
<string name="welcome_unlock_card">Сканировать карту</string>
<string name="welcome_unlock_description">Используйте %s или отсканируйте карту для входа в приложение</string>
<string name="welcome_unlock_title">С возвращением!</string>
<string name="welcome_unlock_title">C возвращением!</string>
</resources>

View file

@ -38,6 +38,7 @@
<string name="card_settings_access_code_recovery_enabled_description">允許您使用此卡重置此錢包中其他卡上的訪問密碼</string>
<string name="card_settings_access_code_recovery_footer">禁用重置此卡或此錢包中其他卡上的訪問密碼的功能</string>
<string name="card_settings_access_code_recovery_title">恢復訪問密碼</string>
<string name="card_settings_action_sheet_reset">重置</string>
<string name="card_settings_action_sheet_title">您確定要這麼做嗎?</string>
<string name="card_settings_change_access_code">更改訪問密碼</string>
<string name="card_settings_change_access_code_footer">訪問密碼將僅在此卡上更改</string>
@ -80,7 +81,6 @@
<string name="common_origin_card">主卡片</string>
<string name="common_reject">拒絕</string>
<string name="common_rename">重新命名</string>
<string name="common_reset">重置</string>
<string name="common_save_changes">保存設置</string>
<string name="common_search">搜索</string>
<string name="common_search_tokens">搜尋代幣</string>
@ -280,7 +280,6 @@
<string name="reset_card_to_factory_warning_message">我了解執行此操作後,我將無法再訪問當前錢包</string>
<string name="reset_card_with_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼</string>
<string name="reset_card_without_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包</string>
<string name="russian_bank_card_warning_subtitle">您有其他國家的銀行卡或銀聯卡嗎?</string>
<string name="russian_bank_card_warning_title">目前不接受俄羅斯銀行卡</string>
<string name="save_user_wallet_agreement_access_description">登錄應用程序並在不掃描卡片的情況下檢查您的資產</string>
<string name="save_user_wallet_agreement_access_title">訪問應用程序</string>
@ -405,7 +404,7 @@
<string name="user_wallet_list_rename_popup_title">重新命名錢包</string>
<string name="user_wallet_list_single_header">單一幣種</string>
<string name="user_wallet_list_title">我的錢包</string>
<string name="user_wallet_list_unlock_all">用 %s 解鎖全部</string>
<string name="user_wallet_list_unlock_all_with">用 %s 解鎖全部</string>
<string name="wallet_address_button_explore">交易記錄</string>
<string name="wallet_balance_blockchain_unreachable">網路無法使用</string>
<string name="wallet_balance_blockchain_unreachable_try_later">區塊鍊無法使用。稍後再試</string>

View file

@ -28,11 +28,16 @@
<string name="app_settings_saved_access_codes_footer">Biometric authentication will be requested instead of the access code for interactions with your card.</string>
<string name="app_settings_saved_wallet">Keep the wallet in the app</string>
<string name="app_settings_saved_wallet_footer">Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.</string>
<string name="app_settings_theme_mode_dark">Dark</string>
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="biometric_lockout_permanent_warning_description">Please scan the card</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
<string name="button_start_backup_process">Start backup process</string>
<plurals name="card_label_card_count">
<item quantity="one">%d card</item>
<item quantity="other">%d cards</item>
@ -41,6 +46,7 @@
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
<string name="card_settings_access_code_recovery_footer">Disable the ability to reset the access code on this card or other cards in this wallet</string>
<string name="card_settings_access_code_recovery_title">Access code recovery</string>
<string name="card_settings_action_sheet_reset">Reset</string>
<string name="card_settings_action_sheet_title">Are you sure you want to do this?</string>
<string name="card_settings_change_access_code">Change Access Code</string>
<string name="card_settings_change_access_code_footer">Access code will be changed on this card only</string>
@ -63,6 +69,7 @@
<string name="common_biometric_authentication">biometric authentication</string>
<string name="common_biometrics">biometrics</string>
<string name="common_buy">Buy</string>
<string name="common_buy_currency">Buy %1$s</string>
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
<string name="common_cancel">Cancel</string>
<string name="common_close">Close</string>
@ -95,7 +102,6 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_reset">Reset</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -129,7 +135,10 @@
<string name="custom_token_creation_error_required_field">Required field</string>
<string name="custom_token_creation_error_wrong_decimals">Decimal number must be a valid integer, no higher than %d</string>
<string name="custom_token_custom_derivation">Custom derivation</string>
<string name="custom_token_custom_derivation_placeholder">E. g. m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Enter custom derivation</string>
<string name="custom_token_decimals_input_title">Decimals</string>
<string name="custom_token_derivation_path">Derivation Path</string>
<string name="custom_token_derivation_path_default">Default</string>
<string name="custom_token_derivation_path_input_title">BIP44 coin type</string>
<string name="custom_token_invalid_derivation_path">The derivation path you\'ve entered is not valid</string>
@ -137,6 +146,8 @@
<string name="custom_token_name_input_title">Name</string>
<string name="custom_token_network_input_not_selected">Not selected</string>
<string name="custom_token_network_input_title">Network</string>
<string name="custom_token_network_selector_title">Token network</string>
<string name="custom_token_subtitle">You can manually add a token that is not natively supported by Tangem</string>
<string name="custom_token_token_symbol_input_placeholder">E.g. USDC</string>
<string name="custom_token_token_symbol_input_title">Token symbol</string>
<string name="custom_token_validation_error_already_added">This token/network has already been added to your list</string>
@ -154,6 +165,8 @@
<string name="details_row_title_cid">Card ID</string>
<string name="details_row_title_create_backup">Link More Cards</string>
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_description_flip_to_hide">Flip your device screen down to quickly hide and show balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_send_feedback">Send Feedback</string>
<string name="details_row_title_signed_hashes">Signed</string>
@ -186,7 +199,7 @@
<string name="initial_message_tap_header">Tap the card</string>
<string name="internal_error_wallet_manager_not_found">Internal error: wallet manager not found</string>
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card to enter</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens.</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
<string name="main_get_bonus_subtitle">You have completed all of the lessons, and are now eligible to receive your 1INCH tokens</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Complete three lessons and receive %d 1INCH token to your wallet</item>
@ -194,7 +207,7 @@
</plurals>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>
<string name="main_no_backup_warning_title">Your wallet hasn\'t been backed up</string>
<string name="main_page_balance">Total balance</string>
<string name="main_processing_full_amount">The amount does not include some of your funds</string>
<string name="main_promotion_credited">1INCH tokens will be credited to your %s wallet address within 48 hours</string>
@ -206,6 +219,30 @@
<item quantity="other">You need to generate addresses for %d new networks using your card</item>
</plurals>
<string name="main_warning_missing_derivation_title">Some addresses are missing</string>
<string name="manage_tokens_add">Add</string>
<string name="manage_tokens_custom">Custom</string>
<string name="manage_tokens_edit">Edit</string>
<string name="manage_tokens_network_selector_native_subtitle">Blockchain the cryptocurrency was initially created</string>
<string name="manage_tokens_network_selector_native_title">Native network</string>
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Not original or primary blockchain the token is hosted</string>
<string name="manage_tokens_network_selector_non_native_title">Non-native networks</string>
<string name="manage_tokens_network_selector_other_subtitle">Blockchain the cryptocurrency was initially created</string>
<string name="manage_tokens_network_selector_other_title">Networks</string>
<string name="manage_tokens_network_selector_title">Choose networks</string>
<string name="manage_tokens_network_selector_wallet">Wallet</string>
<string name="manage_tokens_nothing_found">Couldnt find this token, you can add it manually</string>
<string name="manage_tokens_number_of_wallets">%d of %#@total_wallets@</string>
<plurals name="manage_tokens_number_of_walletstotal_wallets">
<item quantity="one">%d wallet</item>
<item quantity="other">%d wallets</item>
</plurals>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_title">Coin market cap</string>
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
<string name="manage_tokens_unavailable_vote">Upvote</string>
<string name="manage_tokens_wallet_selector_title">Choose wallet</string>
<string name="notification_title_not_enough_funds">Unable to cover %1$s fee</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your wallets</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -233,7 +270,7 @@
<string name="onboarding_button_skip_backup">Skip for later</string>
<string name="onboarding_button_what_does_it_mean">How does it work?</string>
<string name="onboarding_create_wallet_body">Let\'s generate all the keys on your card and create a secure wallet</string>
<string name="onboarding_create_wallet_button_create_wallet">Create a wallet</string>
<string name="onboarding_create_wallet_button_create_wallet">Create wallet</string>
<string name="onboarding_create_wallet_header">Create a wallet</string>
<string name="onboarding_create_wallet_options_button_options">Other options</string>
<string name="onboarding_create_wallet_options_message">Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it.</string>
@ -400,7 +437,7 @@
<string name="story_meet_pay">Pay</string>
<string name="story_meet_send">Send</string>
<string name="story_meet_store">Store</string>
<string name="story_meet_title">Meet\nTangem</string>
<string name="story_meet_title">Meet Tangem</string>
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
<string name="story_web3_title">DeFi Compatible</string>
<string name="swapping_approve_information_text">Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token.</string>
@ -478,7 +515,8 @@
<string name="user_wallet_list_rename_popup_title">Rename Wallet</string>
<string name="user_wallet_list_single_header">Single-currency</string>
<string name="user_wallet_list_title">My Wallets</string>
<string name="user_wallet_list_unlock_all">Unlock all with %s</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="wallet_address_button_explore">Transaction history</string>
<string name="wallet_balance_blockchain_unreachable">Network is unreachable</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
@ -543,6 +581,7 @@
<string name="wallet_title">Tangem</string>
<string name="warning_button_can_be_better">Can be better</string>
<string name="warning_button_learn_more">Learn more</string>
<string name="warning_button_love_it">Love it!</string>
<string name="warning_button_ok">Ok, Got it!</string>
<string name="warning_button_really_cool">Really cool!</string>
<string name="warning_existential_deposit_message">%1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed.</string>
@ -553,7 +592,11 @@
<string name="warning_rate_app_message">How do you like Tangem?</string>
<string name="warning_rate_app_title">One question</string>
<string name="warning_signed_tx_previously">This card has signed transactions in the past</string>
<string name="warning_subtitle_network_unreachable">Network currently is unreachable. Please try again later.</string>
<string name="warning_subtitle_some_networks_unreachable">Some networks currently are unreachable. Please try again later.</string>
<string name="warning_testnet_card_message">This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes.</string>
<string name="warning_title_note_top_up">Note top up</string>
<string name="warning_title_some_networks_unreachable">Some networks are unreachable</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>
<string name="welcome_interrupted_backup_alert_resume">Yes, resume</string>

View file

@ -29,4 +29,5 @@ dependencies {
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.zxing.qrCore)
}

View file

@ -1,44 +1,33 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams
import com.tangem.core.ui.res.TangemTheme
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Simple alert dialog with a message and 'OK' button
@ -129,6 +118,54 @@ fun TextInputDialog(
)
}
@Composable
fun SelectorDialog(
selectedItemIndex: Int,
items: ImmutableList<String>,
confirmButton: DialogButton,
onSelect: (index: Int) -> Unit,
onDismissDialog: () -> Unit,
title: String? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
type = DialogType.Selector(selectedItemIndex, items, onSelect),
confirmButton = confirmButton,
title = title,
onDismissDialog = onDismissDialog,
properties = DialogProperties(
dismissOnBackPress = isDismissable,
dismissOnClickOutside = isDismissable,
),
)
}
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
// region Defaults
@Composable
private fun TangemDialog(
@ -146,15 +183,18 @@ private fun TangemDialog(
shape = TangemTheme.shapes.roundedCornersLarge,
color = TangemTheme.colors.background.plain,
)
.padding(all = TangemTheme.dimens.spacing24),
.padding(vertical = TangemTheme.dimens.spacing24),
) {
if (title != null) {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = title,
style = when (type) {
is DialogType.Message -> TangemTheme.typography.h2
is DialogType.TextInput -> TangemTheme.typography.h3
is DialogType.Selector -> TangemTheme.typography.h2
},
color = TangemTheme.colors.text.primary1,
)
@ -162,7 +202,13 @@ private fun TangemDialog(
}
DialogContent(type = type)
SpacerH24()
DialogButtons(confirmButton = confirmButton, dismissButton = dismissButton)
DialogButtons(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
confirmButton = confirmButton,
dismissButton = dismissButton,
)
}
}
}
@ -177,7 +223,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
when (type) {
is DialogType.Message -> {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = type.message,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
@ -185,7 +233,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
is DialogType.TextInput -> {
OutlineTextField(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
value = type.value,
label = type.params.label,
placeholder = type.params.placeholder,
@ -197,6 +247,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
},
)
}
is DialogType.Selector -> {
SelectorDialogContent(
modifier = Modifier.fillMaxWidth(),
selectedItemIndex = type.selectedItemIndex,
items = type.items,
onSelect = type.onSelect,
)
}
}
}
}
@ -204,7 +262,7 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
@Composable
private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
space = TangemTheme.dimens.spacing4,
alignment = Alignment.End,
@ -252,14 +310,72 @@ private fun DialogButton(
}
}
private sealed interface DialogType {
data class Message(val message: String) : DialogType
@Composable
private fun SelectorDialogContent(
selectedItemIndex: Int,
items: ImmutableList<String>,
onSelect: (index: Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = items) { index, itemText ->
val onClick = remember(index) {
{ onSelect(index) }
}
val interactionSource = remember { MutableInteractionSource() }
Row(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
)
.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing18,
)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
modifier = Modifier.size(TangemTheme.dimens.size24),
selected = index == selectedItemIndex,
onClick = onClick,
colors = RadioButtonDefaults.colors(
selectedColor = TangemTheme.colors.icon.accent,
unselectedColor = TangemTheme.colors.icon.secondary,
),
interactionSource = interactionSource,
)
Text(
text = itemText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Immutable
private sealed class DialogType {
data class Message(val message: String) : DialogType()
data class TextInput(
val value: TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
) : DialogType
) : DialogType()
data class Selector(
val selectedItemIndex: Int,
val items: ImmutableList<String>,
val onSelect: (index: Int) -> Unit,
) : DialogType()
}
// endregion Defaults
@ -381,4 +497,65 @@ private fun TextInputDialogPreview_Dark() {
TextInputDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Light(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = false) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Dark(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = true) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
private class SelctorDialogParamsProvider : CollectionPreviewParameterProvider<SelectorDialogParams>(
collection = listOf(
SelectorDialogParams(
title = "Theme",
selectedItemIndex = 0,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = null,
selectedItemIndex = 2,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = "Count",
selectedItemIndex = 8,
List(size = 10) { it.toString() }.toImmutableList(),
),
),
) {
data class SelectorDialogParams(
val title: String?,
val selectedItemIndex: Int,
val items: ImmutableList<String>,
)
}
// endregion Preview

View file

@ -0,0 +1,29 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.extensions.toQrCode
import com.tangem.core.ui.res.TangemTheme
@Composable
fun rememberQrPainters(
content: List<String>,
size: Dp = TangemTheme.dimens.size248,
padding: Dp = TangemTheme.dimens.spacing0,
): List<BitmapPainter> {
val density = LocalDensity.current
return remember(content) {
content.map { code ->
BitmapPainter(
code.toQrCode(
sizePx = with(density) { size.roundToPx() },
paddingPx = with(density) { padding.roundToPx() },
).asImageBitmap(),
)
}
}
}

View file

@ -1,14 +1,15 @@
package com.tangem.core.ui.components
import androidx.annotation.FloatRange
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@ -54,6 +55,63 @@ fun ResizableText(
)
}
/**
* A Composable function that displays text which can be resized based on its content's overflow.
*
* This function draws text on the screen and checks if it overflows. If the text overflows,
* its font size is reduced recursively until it either fits the available space or reaches a
* specified minimum font size.
*/
@Composable
fun ResizableText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
style: TextStyle = LocalTextStyle.current,
minFontSize: TextUnit = TextUnit.Unspecified,
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
reduceFactor: Double = 0.9,
) {
var fontSize by remember { mutableStateOf(style.fontSize) }
var readyToDraw by remember { mutableStateOf(value = false) }
Text(
modifier = modifier.drawWithContent {
if (readyToDraw) drawContent()
},
text = text,
color = color,
fontSize = fontSize,
textAlign = textAlign,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
style = style,
onTextLayout = { result ->
fun reduceFontSize() {
val reducedFontSize = fontSize * reduceFactor
if (minFontSize != TextUnit.Unspecified && reducedFontSize <= minFontSize) {
fontSize = minFontSize
readyToDraw = true
} else {
fontSize = reducedFontSize
}
}
if (result.hasVisualOverflow) {
reduceFontSize()
} else {
readyToDraw = true
}
},
)
}
data class FontSizeRange(
val min: TextUnit,
val max: TextUnit,

View file

@ -1,15 +1,25 @@
package com.tangem.core.ui.components
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer
import com.valentinilk.shimmer.*
/**
* Rectangle shimmer item with rounded shape from DS
@ -18,11 +28,8 @@ import com.valentinilk.shimmer.shimmer
fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) {
Box(
modifier = modifier
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(size = radius),
),
.clip(RoundedCornerShape(size = radius))
.shimmer(TangemShimmer),
)
}
@ -34,27 +41,67 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim
fun CircleShimmer(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
),
.clip(CircleShape)
.shimmer(TangemShimmer),
)
}
private val TangemShimmer: Shimmer
@Composable
get() = rememberShimmer(
shimmerBounds = ShimmerBounds.View,
theme = defaultShimmerTheme.copy(
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 800,
easing = LinearEasing,
delayMillis = 800,
),
repeatMode = RepeatMode.Restart,
),
shaderColors = TangemShimmerColors,
blendMode = BlendMode.Src,
shaderColorStops = null,
),
)
private val TangemShimmerColors: List<Color>
@Composable
@ReadOnlyComposable
get() {
val isInDarkTheme = LocalIsInDarkTheme.current
return buildList {
if (isInDarkTheme) {
TangemColorPalette.Dark3.let(::add)
TangemColorPalette.Dark4.let(::add)
TangemColorPalette.Dark6.let(::add)
TangemColorPalette.Dark4.let(::add)
TangemColorPalette.Dark3.let(::add)
} else {
TangemColorPalette.Light2.let(::add)
TangemColorPalette.Light1.let(::add)
TangemColorPalette.White.let(::add)
TangemColorPalette.Light1.let(::add)
TangemColorPalette.Light2.let(::add)
}
}
}
// region preview
@Composable
private fun ShimmersPreview() {
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
) {
RectangleShimmer(
modifier = Modifier.size(
width = TangemTheme.dimens.size72,
height = TangemTheme.dimens.size12,
),
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size24),
)
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
}

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import com.tangem.core.ui.res.TangemTheme
/**
* Tangem bottom sheet with custom draggable header and config
*
* @param config data model containing logic and ui models
* @param content custom bottom sheet content
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TangemBottomSheet(
config: TangemBottomSheetConfig,
content: @Composable ColumnScope.(TangemBottomSheetConfigContent) -> Unit,
) {
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheetLarge,
dragHandle = { TangemBottomSheetDraggableHeader() },
) {
content(config.content)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.components.bottomsheets
/**
* Tangem bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
data class TangemBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: TangemBottomSheetConfigContent,
)

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.components.bottomsheets
/**
* General interface for bottom sheet config model
*/
interface TangemBottomSheetConfigContent

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TangemBottomSheetDraggableHeader() {
Surface(
modifier = Modifier
.height(TangemTheme.dimens.size20),
color = TangemTheme.colors.background.primary,
) {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.size(
width = TangemTheme.dimens.size32,
height = TangemTheme.dimens.size4,
)
.background(
color = TangemTheme.colors.icon.inactive,
shape = TangemTheme.shapes.roundedCornersSmall,
),
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
data class AddressModel(
val value: String,
val type: Type = Type.Default,
) {
enum class Type {
Legacy, Default
}
}

View file

@ -0,0 +1,192 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.R
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
if (config.content is TokenReceiveBottomSheetConfig && config.isShow) {
TangemBottomSheet(config) { content ->
TokenReceiveBottomSheetContent(
content = content as TokenReceiveBottomSheetConfig,
)
}
}
}
@Composable
private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing24,
top = TangemTheme.dimens.spacing24,
end = TangemTheme.dimens.spacing24,
bottom = TangemTheme.dimens.spacing16,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24),
) {
QrCodeContent(
content = content,
onAddressChange = { selectedAddress = it },
)
Text(
text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(selectedAddress.value))
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(selectedAddress.value)
},
)
}
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
val pagerState = rememberPagerState()
val pageCount = content.addresses.count()
LaunchedEffect(key1 = pagerState.currentPage) {
onAddressChange.invoke(content.addresses[pagerState.currentPage])
}
HorizontalPager(
pageCount = pageCount,
state = pagerState,
) { currentPage ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
) {
Text(
text = stringResource(
R.string.receive_bottom_sheet_warning_message,
getName(content = content, index = pagerState.currentPage),
content.symbol,
content.network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Image(
painter = qrCodes[currentPage],
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(TangemTheme.dimens.size248),
)
MiddleEllipsisText(
text = content.addresses[currentPage].value,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
if (pageCount > 1) {
val indicatorState = rememberLazyListState()
val selectedColor = TangemTheme.colors.icon.primary1
val unselectedColor = TangemTheme.colors.icon.informative
LazyRow(
modifier = Modifier
.height(TangemTheme.dimens.size20),
state = indicatorState,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pageCount) { iteration ->
item(key = iteration) {
val color = if (pagerState.currentPage == iteration) {
selectedColor
} else {
unselectedColor
}
Box(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing4,
top = TangemTheme.dimens.spacing6,
end = TangemTheme.dimens.spacing4,
bottom = TangemTheme.dimens.spacing6,
)
.background(color, CircleShape)
.size(TangemTheme.dimens.size7),
)
}
}
}
}
}
@Composable
private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String {
return if (content.addresses.size < 2) {
content.name
} else {
"${
stringResource(
id = when (content.addresses[index].type) {
AddressModel.Type.Default -> R.string.address_type_default
AddressModel.Type.Legacy -> R.string.address_type_legacy
},
)
} ${content.name}"
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
class TokenReceiveBottomSheetConfig(
val name: String,
val symbol: String,
val network: String,
val addresses: List<AddressModel>,
) : TangemBottomSheetConfigContent

View file

@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -99,7 +98,7 @@ private fun Button(
val iconTint by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.icon.informative
config.dimContent -> TangemTheme.colors.icon.secondary
config.dimContent -> TangemTheme.colors.icon.informative
else -> TangemTheme.colors.icon.primary1
},
label = "Update tint color",
@ -117,7 +116,7 @@ private fun Button(
val textColor by animateColorAsState(
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
config.dimContent -> TangemTheme.colors.text.secondary
config.dimContent -> TangemTheme.colors.text.tertiary
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",

View file

@ -1,20 +1,19 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerW
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.res.TangemTheme
@Suppress("LongParameterList")
@ -40,31 +39,35 @@ fun TangemButton(
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
val maxContentSize = getMaxButtonContentSize(buttonTextStyle = textStyle)
ButtonContentContainer(
buttonIcon = icon,
iconPadding = size.toIconPadding(),
showProgress = showProgress,
progressIndicator = {
CircularProgressIndicator(
modifier = Modifier.buttonContentSize(textStyle),
modifier = Modifier.buttonContentSize(maxContentSize),
color = colors.contentColor(enabled = enabled).value,
strokeWidth = TangemTheme.dimens.size4,
)
},
text = {
Text(
modifier = Modifier.alignByBaseline(),
ResizableText(
modifier = Modifier
.weight(1f, fill = false)
.heightIn(MinButtonContentSize, maxContentSize),
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
minFontSize = 12.sp,
)
},
icon = { iconResId ->
Icon(
modifier = Modifier.buttonContentSize(textStyle),
modifier = Modifier.buttonContentSize(maxContentSize),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,
@ -89,26 +92,36 @@ private inline fun RowScope.ButtonContentContainer(
} else {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
SpacerW(width = iconPadding)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
}
text()
if (buttonIcon is TangemButtonIconPosition.End) {
SpacerW(width = iconPadding)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(buttonIcon.iconResId)
}
}
}
private fun Modifier.buttonContentSize(buttonTextStyle: TextStyle): Modifier = composed {
val minContentElementSize = TangemTheme.dimens.size20
val maxContentElementSize = remember(key1 = buttonTextStyle.lineHeight) {
buttonTextStyle.lineHeight.value.dp + 4.dp
private val MinButtonContentSize: Dp
@Composable
@ReadOnlyComposable
get() = TangemTheme.dimens.size20
@Composable
@ReadOnlyComposable
private fun getMaxButtonContentSize(buttonTextStyle: TextStyle): Dp {
val buttonLineHeight = with(LocalDensity.current) {
buttonTextStyle.lineHeight.toDp()
}
return buttonLineHeight.coerceAtLeast(MinButtonContentSize)
}
private fun Modifier.buttonContentSize(maxSize: Dp): Modifier = composed {
this.requiredSizeIn(
minWidth = minContentElementSize,
minHeight = minContentElementSize,
maxWidth = maxContentElementSize,
maxHeight = maxContentElementSize,
minWidth = MinButtonContentSize,
minHeight = MinButtonContentSize,
maxWidth = maxSize,
maxHeight = maxSize,
)
}

View file

@ -2,10 +2,10 @@ package com.tangem.core.ui.components.marketprice
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -136,13 +136,17 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
) {
Image(
Icon(
painter = painterResource(
id = when (type) {
PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8
PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8
},
),
tint = when (type) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning
},
contentDescription = null,
)

View file

@ -1,112 +1,140 @@
package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState
/**
* Notification component from Design system.
* Use this for Notification with title, subtitle, clickable or not.
*
* @param state component state
* @param config component config
* @param modifier modifier
* @param iconTint icon tint
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0"
* >Figma component</a>
*/
@Composable
fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius18))
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius18),
)
.clickable(
enabled = when (state) {
is NotificationState.Clickable -> true
is NotificationState.Simple, is NotificationState.Closable -> false
},
onClick = if (state is NotificationState.Clickable) {
state.onClick
} else {
{}
},
),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8),
fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) {
BaseContainer(buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier) {
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
) {
NotificationIcon(
iconResId = state.iconResId,
iconTint = state.tint,
MainContent(
iconResId = config.iconResId,
iconTint = iconTint,
title = config.title,
subtitle = config.subtitle,
isClickableComponent = config.onClick != null,
)
Buttons(state = config.buttonsState)
}
CloseableIconButton(
onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
)
}
}
@Composable
private fun BaseContainer(
buttonsState: NotificationConfig.ButtonsState?,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val containerColor by rememberUpdatedState(
newValue = if (buttonsState != null || onClick != null) {
TangemTheme.colors.background.primary
} else {
TangemTheme.colors.button.disabled
},
)
Surface(
onClick = onClick ?: {},
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
.fillMaxWidth(),
enabled = onClick != null,
shape = TangemTheme.shapes.roundedCornersXMedium,
color = containerColor,
) {
Box(content = content)
}
}
@Composable
private fun MainContent(
iconResId: Int,
iconTint: Color?,
title: TextReference,
subtitle: TextReference,
isClickableComponent: Boolean,
) {
Row {
Icon(
iconResId = iconResId,
tint = iconTint,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
SpacerW(width = TangemTheme.dimens.spacing10)
TextsBlock(title = title, subtitle = subtitle)
if (isClickableComponent) {
SpacerWMax()
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.align(alignment = Alignment.CenterStart),
.align(alignment = Alignment.CenterVertically),
tint = TangemTheme.colors.icon.informative,
)
NotificationInfoBlock(
title = state.title.resolveReference(),
subtitle = state.subtitle?.resolveReference(),
modifier = Modifier.align(alignment = Alignment.CenterStart),
)
if (state is NotificationState.Closable) {
Icon(
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.align(alignment = Alignment.TopEnd),
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
if (state is NotificationState.Clickable) {
Icon(
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.align(alignment = Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
}
@Composable
private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modifier: Modifier = Modifier) {
if (iconTint != null) {
private fun Icon(@DrawableRes iconResId: Int, tint: Color?, modifier: Modifier = Modifier) {
if (tint != null) {
Icon(
painter = painterResource(id = iconResId),
contentDescription = null,
modifier = modifier,
tint = iconTint,
tint = tint,
)
} else {
Image(
@ -118,20 +146,104 @@ private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modi
}
@Composable
private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing30)) {
private fun TextsBlock(title: TextReference, subtitle: TextReference) {
Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) {
Text(
text = title,
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
style = TangemTheme.typography.button,
)
if (!subtitle.isNullOrEmpty()) {
SpacerH2()
Text(
text = subtitle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
Text(
text = subtitle.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
)
}
}
@Composable
private fun Buttons(state: NotificationButtonsState?) {
when (state) {
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state)
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state)
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state)
null -> Unit
}
}
@Composable
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) {
SecondaryButton(
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) {
if (config.iconResId != null) {
PrimaryButtonIconEnd(
text = config.text.resolveReference(),
iconResId = config.iconResId,
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
)
} else {
PrimaryButton(
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
SecondaryButton(
text = config.secondaryText.resolveReference(),
onClick = config.onSecondaryClick,
modifier = Modifier.weight(weight = 1f),
)
PrimaryButton(
text = config.primaryText.resolveReference(),
onClick = config.onPrimaryClick,
modifier = Modifier.weight(weight = 1f),
)
}
}
@Composable
private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = onClick != null, modifier = modifier) {
onClick ?: return@AnimatedVisibility
/*
* Implement a custom ripple because the design layout doesn't match the Material Design.
* Material Icon has a size 24x24 and Material IconButton has a size 48x48,
* but icon from Figma has a size 16x16.
*/
Box(
modifier = Modifier
.size(size = TangemTheme.dimens.size40)
.clip(shape = CircleShape)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current,
role = Role.Button,
onClick = onClick,
),
) {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
modifier = Modifier
.size(size = TangemTheme.dimens.size16)
.align(alignment = Alignment.Center),
tint = TangemTheme.colors.icon.inactive,
)
}
}
@ -139,72 +251,83 @@ private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Mo
@Preview
@Composable
private fun Preview_WarningNotification_Light(
@PreviewParameter(NotificationStateProvider::class)
state: NotificationState,
private fun Preview_Notification_Light(
@PreviewParameter(NotificationConfigProvider::class)
config: NotificationConfig,
) {
TangemTheme(isDark = false) {
Notification(state)
Notification(config)
}
}
@Preview
@Composable
private fun Preview_WarningNotification_Dark(
@PreviewParameter(NotificationStateProvider::class)
state: NotificationState,
private fun Preview_Notification_Dark(
@PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig,
) {
TangemTheme(isDark = true) {
Notification(state)
Notification(config)
}
}
private class NotificationStateProvider : CollectionPreviewParameterProvider<NotificationState>(
private class NotificationConfigProvider : CollectionPreviewParameterProvider<NotificationConfig>(
collection = listOf(
NotificationState.Simple(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
NotificationConfig(
title = TextReference.Str(value = "Development card"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
"ut labore et...",
value = "The card you scanned is a development card.\nDont accept it as a payment.",
),
iconResId = R.drawable.img_attention_20,
),
NotificationState.Simple(
title = TextReference.Str("Your wallet hasnt been backed up"),
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
NotificationState.Clickable(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
"ut labore et...",
),
NotificationConfig(
title = TextReference.Str(value = "Some networks are unreachable"),
subtitle = TextReference.Str(value = "Check your network connection"),
iconResId = R.drawable.img_attention_20,
),
NotificationConfig(
title = TextReference.Str(value = "Used card"),
subtitle = TextReference.Str(value = "The card signed transactions in the past"),
iconResId = R.drawable.ic_alert_circle_24,
onClick = {},
),
NotificationState.Clickable(
NotificationConfig(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
onClick = {},
),
NotificationState.Closable(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
"ut labore et...",
),
subtitle = TextReference.Str(value = "To protect your assets, we advise you to carry out this procedure"),
iconResId = R.drawable.img_attention_20,
onCloseClick = {},
buttonsState = NotificationButtonsState.SecondaryButtonConfig(
text = TextReference.Str(value = "Start backup process"),
onClick = {},
),
),
NotificationState.Closable(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = null,
NotificationConfig(
title = TextReference.Str(value = "Some addresses are missing"),
subtitle = TextReference.Str(value = "Generate addresses for 2 new networks using your card"),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
buttonsState = NotificationButtonsState.PrimaryButtonConfig(
text = TextReference.Str(value = "Generate addresses"),
iconResId = R.drawable.ic_tangem_24,
onClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Rate the app"),
subtitle = TextReference.Str(value = "How do you like Tangem?"),
iconResId = R.drawable.img_attention_20,
buttonsState = NotificationButtonsState.PairButtonsConfig(
primaryText = TextReference.Str(value = "Love it!"),
onPrimaryClick = {},
secondaryText = TextReference.Str(value = "Can be better"),
onSecondaryClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Note top up"),
subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = NotificationButtonsState.SecondaryButtonConfig(
text = TextReference.Str(value = "Top up card"),
onClick = {},
),
onCloseClick = {},
),
),

View file

@ -0,0 +1,44 @@
package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
/**
* Notification component state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property buttonsState buttons state
* @property onClick lambda be invoked when notification is clicked
* @property onCloseClick lambda be invoked when close button is clicked
*
[REDACTED_AUTHOR]
*/
data class NotificationConfig(
val title: TextReference,
val subtitle: TextReference,
@DrawableRes val iconResId: Int,
val buttonsState: ButtonsState? = null,
val onClick: (() -> Unit)? = null,
val onCloseClick: (() -> Unit)? = null,
) {
sealed class ButtonsState {
data class PrimaryButtonConfig(
val text: TextReference,
@DrawableRes val iconResId: Int? = null,
val onClick: () -> Unit,
) : ButtonsState()
data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState()
data class PairButtonsConfig(
val primaryText: TextReference,
val onPrimaryClick: () -> Unit,
val secondaryText: TextReference,
val onSecondaryClick: () -> Unit,
) : ButtonsState()
}
}

View file

@ -1,72 +0,0 @@
package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
/**
* Notification component state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
*
[REDACTED_AUTHOR]
*/
sealed class NotificationState(
open val title: TextReference,
open val subtitle: TextReference? = null,
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
) {
/**
* Simple notification state. Non clickable.
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
*/
data class Simple(
override val title: TextReference,
override val subtitle: TextReference? = null,
@DrawableRes override val iconResId: Int,
override val tint: Color? = null,
) : NotificationState(title, subtitle, iconResId, tint)
/**
* Clickable notification state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
* @property onClick lambda be invoked when notification component is clicked
*/
data class Clickable(
override val title: TextReference,
override val subtitle: TextReference? = null,
@DrawableRes override val iconResId: Int,
override val tint: Color? = null,
val onClick: () -> Unit,
) : NotificationState(title, subtitle, iconResId, tint)
/**
* Closable notification state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
* @property onCloseClick lambda be invoked when close button is clicked
*/
data class Closable(
override val title: TextReference,
override val subtitle: TextReference? = null,
@DrawableRes override val iconResId: Int,
override val tint: Color? = null,
val onCloseClick: (() -> Unit)? = null,
) : NotificationState(title, subtitle, iconResId, tint)
}

View file

@ -35,10 +35,7 @@ fun LazyListScope.txHistoryItems(
)
}
is TxHistoryState.Empty -> {
nonContentItem(
state = EmptyTransactionsBlockState.Empty(onClick = state.onBuyClick),
modifier = modifier,
)
nonContentItem(state = EmptyTransactionsBlockState.Empty, modifier = modifier)
}
is TxHistoryState.Error -> {
nonContentItem(

View file

@ -47,7 +47,9 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier
color = TangemTheme.colors.text.secondary,
)
ActionButton(config = state.actionButtonConfig)
state.actionButtonConfig?.let {
ActionButton(config = it)
}
}
}
@ -73,7 +75,7 @@ private fun EmptyTransactionBlock_Dark(
private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider<EmptyTransactionsBlockState>(
collection = listOf(
EmptyTransactionsBlockState.Empty(onClick = {}),
EmptyTransactionsBlockState.Empty,
EmptyTransactionsBlockState.FailedToLoad(onClick = {}),
EmptyTransactionsBlockState.NotImplemented(onClick = {}),
),

View file

@ -5,12 +5,12 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
sealed class EmptyTransactionsBlockState(
val actionButtonConfig: ActionButtonConfig? = null,
val iconRes: Int,
val text: TextReference,
val actionButtonConfig: ActionButtonConfig,
) {
class FailedToLoad(onClick: () -> Unit) : EmptyTransactionsBlockState(
data class FailedToLoad(val onClick: () -> Unit) : EmptyTransactionsBlockState(
actionButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_reload),
iconResId = R.drawable.ic_refresh_24,
@ -21,18 +21,12 @@ sealed class EmptyTransactionsBlockState(
text = TextReference.Res(R.string.transaction_history_error_failed_to_load),
)
class Empty(onClick: (() -> Unit)?) : EmptyTransactionsBlockState(
actionButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_buy),
iconResId = R.drawable.ic_plus_24,
onClick = onClick ?: {},
enabled = onClick != null,
),
iconRes = R.drawable.img_coin_64,
object Empty : EmptyTransactionsBlockState(
iconRes = R.drawable.ic_empty_token_64,
text = TextReference.Res(R.string.transaction_history_empty_transactions),
)
class NotImplemented(onClick: () -> Unit) : EmptyTransactionsBlockState(
data class NotImplemented(val onClick: () -> Unit) : EmptyTransactionsBlockState(
actionButtonConfig = ActionButtonConfig(
text = TextReference.Res(R.string.common_explore_transaction_history),
iconResId = R.drawable.ic_arrow_top_right_24,

View file

@ -1,10 +0,0 @@
package com.tangem.core.ui.components.transactions.intents
interface TxHistoryClickIntents {
fun onBuyClick()
fun onReloadClick()
fun onExploreClick()
}

View file

@ -13,12 +13,8 @@ sealed interface TxHistoryState {
*/
data class Content(val contentItems: MutableStateFlow<PagingData<TxHistoryItemState>>) : TxHistoryState
/**
* Empty state
*
* @property onBuyClick lambda be invoke when buy button was clicked
*/
data class Empty(val onBuyClick: () -> Unit) : TxHistoryState
/** Empty state */
object Empty : TxHistoryState
/**
* Not supported tx history state

View file

@ -13,11 +13,12 @@ import androidx.compose.runtime.NonRestartableComposable
*/
@Composable
@NonRestartableComposable
fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) {
@Suppress("UnnecessaryEventHandlerParameter")
fun <A> EventEffect(event: StateEvent<A>, onTrigger: suspend (data: A) -> Unit) {
LaunchedEffect(event) {
if (event is StateEvent.Triggered) {
onTrigger()
event.consume()
if (event is StateEvent.Triggered<A>) {
onTrigger(event.data)
event.onConsume()
}
}
}

View file

@ -9,32 +9,34 @@ import androidx.compose.runtime.Immutable
* re-triggered on recompositions or state changes.
*/
@Immutable
sealed class StateEvent {
/** Defines the action to be executed when the event is consumed. */
protected abstract val onConsume: () -> Unit
sealed class StateEvent<in A> {
/**
* Represents an already consumed state event.
* Events of this type will not trigger any further actions.
*/
object Consumed : StateEvent() {
override val onConsume: () -> Unit = {}
class Consumed<A> : StateEvent<A>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is Consumed<*>
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
/**
* Represents a state event that has been triggered but not yet consumed.
*
* @property data The data provided by the event.
* @property onConsume The action to be executed when the event is consumed.
*/
data class Triggered(override val onConsume: () -> Unit) : StateEvent()
/**
* Consumes the event, triggering any associated action.
*/
fun consume() {
onConsume()
}
data class Triggered<A>(
val data: A,
internal val onConsume: () -> Unit,
) : StateEvent<A>()
}
/**
@ -43,9 +45,11 @@ sealed class StateEvent {
* @param onConsume The action to be executed when the event is consumed.
* @return A triggered state event.
*/
fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume)
fun <A> triggeredEvent(data: A, onConsume: () -> Unit): StateEvent<A> = StateEvent.Triggered(data, onConsume)
/**
* Represents a statically defined [StateEvent.Consumed] event.
* Creates a [StateEvent.Consumed] instance.
*
* @return A consumed state event.
*/
val consumed: StateEvent.Consumed = StateEvent.Consumed
fun <A> consumedEvent(): StateEvent<A> = StateEvent.Consumed()

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.extensions
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ContextCompat.startActivity(this, shareIntent, null)
}

View file

@ -1,45 +1,52 @@
package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.models.CryptoCurrency
private const val LIGHT_LUMINANCE = 0.5f
private const val COLOR_HEX_START_INDEX = 2
private const val COLOR_HEX_END_INDEX = 7
/**
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
* Retrieves the resource ID for the network of a [CryptoCurrency].
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the network badge of a given cryptocurrency. For coins, this will typically
* return null as they do not have network badges, while tokens will fetch the icon
* based on their associated network ID.
*
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
* @return Drawable resource ID for the network.
*/
@get:DrawableRes
val CryptoCurrency.networkBadgeIconResId: Int?
get() = when (this) {
is CryptoCurrency.Coin -> null
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
val CryptoCurrency.networkIconResId: Int
get() = getActiveIconRes(network.id.value)
/**
* Tries to extract a background color from the contract address of a token.
*
* @param fallbackColor The color to use as a fallback.
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
*/
fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(
isGrayscale: Boolean,
fallbackColor: Color = TangemColorPalette.Black,
): Color {
if (isGrayscale) return TangemColorPalette.Dark2
return try {
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
Color(colorHex.toColorInt())
} catch (exception: Exception) {
fallbackColor
}
}
/**
* Retrieves the resource ID for the icon of a [CryptoCurrency].
* Determines the tint color to be used for a token icon based on its background color.
* If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen.
*
* This property provides a way to fetch the appropriate drawable resource ID
* for the icon of a given cryptocurrency.
*
* @return Drawable resource ID for the cryptocurrency icon.
* @param iconBackground The background color of the custom token icon.
* @return The tint color to be used for the icon.
*/
@get:DrawableRes
val CryptoCurrency.iconResId: Int
get() = when (this) {
is CryptoCurrency.Coin -> {
val rawCoinId = id.rawCurrencyId
if (rawCoinId != null) {
getActiveIconResByCoinId(rawCoinId, network.id.value)
} else {
R.drawable.ic_alert_24
}
}
is CryptoCurrency.Token -> R.drawable.ic_alert_24
}
fun getTintForTokenIcon(iconBackground: Color): Color {
return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.extensions
import android.graphics.Bitmap
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.Hashtable
@Suppress("MagicNumber")
fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap {
val hintMap = Hashtable<EncodeHintType, Any>()
hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage
hintMap[EncodeHintType.MARGIN] = paddingPx
val qrCodeWriter = QRCodeWriter()
val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, sizePx, sizePx, hintMap)
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until width) {
bmp.setPixel(
y,
x,
if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE,
)
}
}
return bmp
}

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.extensions
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
@ -48,6 +49,59 @@ sealed interface TextReference {
* @see [TextReference.plus] method
*/
data class Combined(val refs: WrappedList<TextReference>) : TextReference
companion object {
/** Empty string as [TextReference] */
val EMPTY: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = "") }
}
}
/**
* Creates a [TextReference] using a string resource ID with optional format arguments.
*
* @param id The resource ID of the string.
* @param formatArgs A list of format arguments to be applied to the string resource.
* @return A [TextReference] representing the string resource with format arguments.
*/
fun resourceReference(@StringRes id: Int, formatArgs: WrappedList<Any> = WrappedList(emptyList())): TextReference {
return TextReference.Res(id, formatArgs)
}
/**
* Creates a [TextReference] using a plain string value.
*
* @param value The plain string value.
* @return A [TextReference] representing the provided string value.
*/
fun stringReference(value: String): TextReference {
return TextReference.Str(value)
}
/**
* Creates a [TextReference] using a plural string resource ID with count and optional format arguments.
*
* @param id The resource ID of the plural string.
* @param count The count value to determine the plural form.
* @param formatArgs A list of format arguments to be applied to the plural string resource.
* @return A [TextReference] representing the plural string resource with count and format arguments.
*/
fun pluralReference(
@PluralsRes id: Int,
count: Int,
formatArgs: WrappedList<Any> = WrappedList(emptyList()),
): TextReference {
return TextReference.PluralRes(id, count, formatArgs)
}
/**
* Combines multiple [TextReference] instances into a single [TextReference].
*
* @param refs A list of [TextReference] instances to be combined.
* @return A [TextReference] representing the combined text references.
*/
fun combinedReference(refs: WrappedList<TextReference>): TextReference {
return TextReference.Combined(refs)
}
/** Resolve [TextReference] as [String] */
@ -55,7 +109,13 @@ sealed interface TextReference {
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
stringResource(id = id, *args)
}
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Combined -> {
@ -68,6 +128,28 @@ fun TextReference.resolveReference(): String {
}
}
/** Resolve [TextReference] as [String] using [resources] (non-composable context) */
fun TextReference.resolveReference(resources: Resources): String {
return when (this) {
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference(resources) else it }
.toTypedArray()
resources.getString(id, *args)
}
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Combined -> {
buildString {
refs.forEach {
append(it.resolveReference(resources))
}
}
}
}
}
/** Concatenate [this] reference with [ref] */
operator fun TextReference.plus(ref: TextReference): TextReference {
return when (this) {

View file

@ -4,6 +4,7 @@ import androidx.compose.material.Colors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemColorPalette.Amaranth
import com.tangem.core.ui.res.TangemColorPalette.Azure
import com.tangem.core.ui.res.TangemColorPalette.Black
import com.tangem.core.ui.res.TangemColorPalette.Dark1
import com.tangem.core.ui.res.TangemColorPalette.Dark2
@ -11,7 +12,6 @@ import com.tangem.core.ui.res.TangemColorPalette.Dark4
import com.tangem.core.ui.res.TangemColorPalette.Dark6
import com.tangem.core.ui.res.TangemColorPalette.Light4
import com.tangem.core.ui.res.TangemColorPalette.Light5
import com.tangem.core.ui.res.TangemColorPalette.Meadow
import com.tangem.core.ui.res.TangemColorPalette.Tangerine
import com.tangem.core.ui.res.TangemColorPalette.White
@ -22,7 +22,7 @@ enum class IconColorType(val lightColor: Color, val darkColor: Color) {
SECONDARY(lightColor = Dark2, darkColor = Dark1),
INFORMATIVE(lightColor = Light5, darkColor = Dark2),
INACTIVE(lightColor = Light4, darkColor = Dark4),
ACCENT(lightColor = Meadow, darkColor = Meadow),
ACCENT(lightColor = Azure, darkColor = Azure),
WARNING(lightColor = Amaranth, darkColor = Amaranth),
ATTENTION(lightColor = Tangerine, darkColor = Tangerine),
}

View file

@ -33,16 +33,16 @@ object TangemColorPalette {
// endregion Green
// region Blue
val Cobalt = Color(0xFF0029FF)
val Azure = Color(0xFF007AFF)
val Azure = Color(0xFF0099FF)
// endregion Blue
// region Red
val Amaranth = Color(0xFFDE1010)
val Flamingo = Color(0xFFED7979)
val Amaranth = Color(0xFFFF3333)
val Flamingo = Color(0xFFFF5B5B)
// endregion Red
// region Yellow
val Tangerine = Color(0xFFFFB71B)
val Mustard = Color(0xFFFDDE55)
// endregion Yellow
}

View file

@ -39,9 +39,9 @@ class TangemColors internal constructor(
secondary: Color,
tertiary: Color,
disabled: Color,
accent: Color = TangemColorPalette.Meadow,
warning: Color = TangemColorPalette.Amaranth,
attention: Color = TangemColorPalette.Tangerine,
warning: Color,
attention: Color,
accent: Color = TangemColorPalette.Azure,
constantWhite: Color = TangemColorPalette.White,
) {
var primary1 by mutableStateOf(primary1)
@ -80,9 +80,9 @@ class TangemColors internal constructor(
secondary: Color,
informative: Color,
inactive: Color,
accent: Color = TangemColorPalette.Meadow,
warning: Color = TangemColorPalette.Amaranth,
attention: Color = TangemColorPalette.Tangerine,
warning: Color,
attention: Color,
accent: Color = TangemColorPalette.Azure,
) {
var primary1 by mutableStateOf(primary1)
private set

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Suppress("ConstructorParameterNaming")
@Suppress("ConstructorParameterNaming", "MagicNumber")
@Immutable
data class TangemDimens internal constructor(
// region Elevation
@ -81,6 +81,7 @@ data class TangemDimens internal constructor(
val size158: Dp = 158.dp,
val size164: Dp = 164.dp,
val size200: Dp = 200.dp,
val size248: Dp = 248.dp,
// endregion Size
// region Spacing
val spacing0: Dp = 0.dp,

View file

@ -13,6 +13,7 @@ data class TangemShapes internal constructor(
val roundedCornersXMedium: Shape,
val roundedCornersLarge: Shape,
val bottomSheet: Shape,
val bottomSheetLarge: Shape,
) {
constructor(dimens: TangemDimens) : this(
roundedCornersSmall = RoundedCornerShape(size = dimens.radius2),
@ -25,5 +26,9 @@ data class TangemShapes internal constructor(
topStart = dimens.radius16,
topEnd = dimens.radius16,
),
bottomSheetLarge = RoundedCornerShape(
topStart = dimens.radius24,
topEnd = dimens.radius24,
),
)
}

View file

@ -36,6 +36,7 @@ fun TangemTheme(
LocalTangemTypography provides typography,
LocalTangemDimens provides dimens,
LocalTangemShapes provides shapes,
LocalIsInDarkTheme provides isDark,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -88,6 +89,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@ -96,6 +98,8 @@ private fun lightThemeColors(): TangemColors {
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark1,
disabled = TangemColorPalette.Light4,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
icon = TangemColors.Icon(
primary1 = TangemColorPalette.Black,
@ -103,6 +107,8 @@ private fun lightThemeColors(): TangemColors {
secondary = TangemColorPalette.Dark2,
informative = TangemColorPalette.Light5,
inactive = TangemColorPalette.Light4,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
button = TangemColors.Button(
primary = TangemColorPalette.Dark6,
@ -135,6 +141,7 @@ private fun lightThemeColors(): TangemColors {
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@ -143,6 +150,8 @@ private fun darkThemeColors(): TangemColors {
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Dark1,
disabled = TangemColorPalette.Dark3,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
icon = TangemColors.Icon(
primary1 = TangemColorPalette.White,
@ -150,6 +159,8 @@ private fun darkThemeColors(): TangemColors {
secondary = TangemColorPalette.Dark1,
informative = TangemColorPalette.Dark2,
inactive = TangemColorPalette.Dark4,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
button = TangemColors.Button(
primary = TangemColorPalette.Light1,
@ -195,4 +206,6 @@ private val LocalTangemDimens = staticCompositionLocalOf {
private val LocalTangemShapes = staticCompositionLocalOf<TangemShapes> {
error("No TangemShapes provided")
}
}
val LocalIsInDarkTheme = staticCompositionLocalOf { false }

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="9dp"
android:height="8dp"
android:viewportWidth="9"
android:viewportHeight="8">
<path
android:fillColor="#000"
android:pathData="M4.6,6.9C4.8,7.2 5.2,7.2 5.4,6.9L8.9,1.7C9.1,1.3 8.9,0.9 8.5,0.9L1.4,0.9C1,0.9 0.8,1.4 1,1.7L4.6,6.9Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="9dp"
android:height="8dp"
android:viewportWidth="9"
android:viewportHeight="8">
<path
android:fillColor="#000"
android:pathData="M4.6,1.1C4.8,0.8 5.2,0.8 5.4,1.1L8.9,6.3C9.1,6.7 8.9,7.1 8.5,7.1L1.4,7.1C1,7.1 0.8,6.7 1,6.3L4.6,1.1Z" />
</vector>

View file

@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:pathData="M7,15H9C9,16.08 10.37,17 12,17C13.63,17 15,16.08 15,15C15,13.9 13.96,13.5 11.76,12.97C9.64,12.44 7,11.78 7,9C7,7.21 8.47,5.69 10.5,5.18V3H13.5V5.18C15.53,5.69 17,7.21 17,9H15C15,7.92 13.63,7 12,7C10.37,7 9,7.92 9,9C9,10.1 10.04,10.5 12.24,11.03C14.36,11.56 17,12.22 17,15C17,16.79 15.53,18.31 13.5,18.82V21H10.5V18.82C8.47,18.31 7,16.79 7,15Z"
android:fillColor="#1E1E1E" />
android:fillColor="#000000" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:fillColor="#656565"
android:pathData="M22,34L22.69,29.327C23,27.23 23.155,26.182 23.648,25.352C24.065,24.651 24.651,24.065 25.352,23.648C26.182,23.155 27.23,23 29.327,22.69L34,22L29.327,21.31C27.23,21 26.182,20.845 25.352,20.352C24.651,19.935 24.065,19.349 23.648,18.648C23.155,17.818 23,16.77 22.69,14.673L22,10L21.31,14.673C21,16.77 20.845,17.818 20.352,18.648C19.935,19.349 19.349,19.935 18.648,20.352C17.818,20.845 16.77,21 14.673,21.31L10,22L14.673,22.69C16.77,23 17.818,23.155 18.648,23.648C19.349,24.065 19.935,24.651 20.352,25.352C20.845,26.182 21,27.23 21.31,29.327L22,34Z" />
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<group>
<clip-path android:pathData="M0,0h64v64h-64z" />
<path
android:pathData="M37.84,55.346C50.625,52.278 58.482,39.342 55.389,26.453C52.296,13.564 39.424,5.603 26.639,8.671C20.067,10.248 14.797,14.433 11.647,19.879M11.647,19.879L22.5,19.879M11.647,19.879L11.647,8.001"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#C9C9C9"
android:strokeLineCap="round" />
<path
android:pathData="M9.945,22.612C10.593,22.85 10.926,23.568 10.688,24.216L10.651,24.317C10.416,24.966 9.699,25.302 9.05,25.067C8.401,24.832 8.065,24.115 8.3,23.466L8.341,23.354C8.579,22.706 9.297,22.374 9.945,22.612ZM8.025,31.53C8.715,31.509 9.291,32.051 9.312,32.742L9.314,32.795L9.315,32.84C9.339,33.53 8.799,34.108 8.109,34.132C7.419,34.156 6.84,33.616 6.816,32.926L6.815,32.877L6.813,32.817C6.792,32.127 7.335,31.551 8.025,31.53ZM9.036,38.993C9.686,38.759 10.401,39.097 10.634,39.747L10.665,39.83C10.9,40.479 10.565,41.196 9.917,41.432C9.268,41.667 8.551,41.333 8.315,40.684L8.282,40.591C8.048,39.942 8.386,39.226 9.036,38.993ZM12.309,45.763C12.854,45.34 13.639,45.439 14.062,45.984L14.116,46.054C14.542,46.598 14.446,47.383 13.902,47.808C13.358,48.234 12.572,48.138 12.147,47.594L12.087,47.516C11.664,46.971 11.763,46.186 12.309,45.763ZM17.512,51.191C17.901,50.621 18.679,50.474 19.249,50.863L19.322,50.912C19.894,51.299 20.044,52.076 19.658,52.648C19.271,53.22 18.494,53.37 17.922,52.983L17.841,52.928C17.271,52.539 17.124,51.762 17.512,51.191ZM24.135,54.753C24.328,54.09 25.022,53.709 25.685,53.902L25.769,53.927C26.433,54.117 26.817,54.81 26.626,55.473C26.436,56.137 25.743,56.52 25.08,56.33L24.986,56.303C24.323,56.11 23.942,55.416 24.135,54.753ZM31.537,56.082C31.516,55.392 32.058,54.815 32.748,54.794L32.792,54.792L32.837,54.791C33.527,54.767 34.105,55.307 34.129,55.997C34.153,56.687 33.613,57.266 32.923,57.29L32.874,57.291L32.825,57.293C32.134,57.314 31.558,56.772 31.537,56.082Z"
android:fillColor="#C9C9C9"
android:fillType="evenOdd" />
<path
android:pathData="M23.528,30.572L32.5,19.036L41.472,30.572L32.5,44.672L23.528,30.572Z"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#C9C9C9" />
</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="M21,8L17,4V7H9V9H17V12L21,8ZM7,12L3,16L7,20V17H15V15H7V12Z"
android:fillColor="#000000"/>
</vector>

View file

@ -1,9 +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="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27ZM12,9C12.796,9 13.559,9.316 14.121,9.879C14.684,10.441 15,11.204 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9ZM12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5ZM3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z"
android:fillColor="#000000"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="9dp"
android:height="8dp"
android:viewportWidth="9"
android:viewportHeight="8">
<path
android:pathData="M4.6114,6.8903C4.8128,7.1793 5.2419,7.175 5.4375,6.8821L8.9151,1.6731C9.1369,1.3409 8.8988,0.8955 8.4993,0.8955L1.3921,0.8955C0.9881,0.8955 0.7509,1.3499 0.9819,1.6814L4.6114,6.8903Z"
android:fillColor="#FF3333"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="9dp"
android:height="8dp"
android:viewportWidth="9"
android:viewportHeight="8">
<path
android:pathData="M4.6114,1.1097C4.8128,0.8207 5.2419,0.825 5.4375,1.1179L8.9151,6.3269C9.1369,6.6591 8.8988,7.1045 8.4993,7.1045L1.3921,7.1045C0.9881,7.1045 0.7509,6.6501 0.9819,6.3186L4.6114,1.1097Z"
android:fillColor="#1ACE80"/>
</vector>

View file

@ -3,6 +3,8 @@ package com.tangem.utils.extensions
/**
* Removes an element from the collection based on the provided predicate.
*
* !!!This function is not thread-safe!!!
*
* @param predicate The condition to remove an element.
* @return [Boolean] indicating whether an element was removed.
*/
@ -33,7 +35,8 @@ inline fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boo
/**
* Adds the specified element to the list or replaces an existing element.
* The predicate defines the condition to replace the existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.

View file

@ -0,0 +1,39 @@
package com.tangem.utils.extensions
/**
* Replaces an element in the set with the provided item based on the predicate.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to replace the existing one.
* @param predicate The condition to replace an existing element.
* @return [Boolean] indicating whether an element was replaced.
*/
inline fun <T> MutableSet<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val foundItem = firstOrNull(predicate) ?: return false
remove(foundItem)
add(item)
return true
}
/**
* Adds the specified element to the set or replaces an existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.
* @return The modified [Set] after adding or replacing the element.
*/
inline fun <T> Set<T>.addOrReplace(item: T, predicate: (T) -> Boolean): Set<T> {
val mutableList = this.toMutableSet()
val isReplaced = mutableList.replaceBy(item, predicate)
if (!isReplaced) {
mutableList.add(item)
}
return mutableList
}