Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:07:03 +03:00
commit f0d03e3648
286 changed files with 6353 additions and 5898 deletions

View file

@ -11,6 +11,7 @@ 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)

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

@ -28,7 +28,7 @@ class NetworkModule {
fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(

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": "4.11.0"

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>
@ -96,7 +102,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>
@ -196,7 +201,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 +215,7 @@
<item quantity="other">Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту</item>
</plurals>
<string name="main_warning_missing_derivation_title">Некоторые адреса отсутствуют</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>
@ -319,8 +325,6 @@
<item quantity="other">за %d кошельков</item>
</plurals>
<string name="referral_point_currencies_description">Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг</string>
<string name="referral_point_currencies_description_prefix">Получите</string>
<string name="referral_point_currencies_description_suffix">на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг</string>
<string name="referral_point_currencies_title">Вы</string>
<string name="referral_point_discount_description_prefix">Получит</string>
<string name="referral_point_discount_description_suffix">при покупке кошелька на сайте tangem.com</string>
@ -407,7 +411,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>

View file

@ -80,7 +80,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>
@ -259,8 +258,6 @@
<string name="referral_error_failed_to_load_info">無法加載有關推薦計劃的消息。請稍後再試</string>
<string name="referral_error_failed_to_load_info_with_reason">無法加載有關推薦計劃的消息。原因:%s。請稍後再試</string>
<string name="referral_friends_bought_title">您的朋友買</string>
<string name="referral_point_currencies_description_prefix">會得到</string>
<string name="referral_point_currencies_description_suffix">對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包</string>
<string name="referral_point_currencies_title"></string>
<string name="referral_point_discount_description_prefix">得到</string>
<string name="referral_point_discount_description_value">%s 折扣</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>
@ -95,7 +101,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>
@ -194,7 +199,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 +211,7 @@
<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="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 +239,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>
@ -314,8 +320,6 @@
<item quantity="other">for %d wallets</item>
</plurals>
<string name="referral_point_currencies_description">Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that</string>
<string name="referral_point_currencies_description_prefix">Will get</string>
<string name="referral_point_currencies_description_suffix">for each wallet bought by your friend on your %1$s network address%2$s</string>
<string name="referral_point_currencies_title">You</string>
<string name="referral_point_discount_description_prefix">Will get a</string>
<string name="referral_point_discount_description_suffix">when buying a wallet on tangem.com</string>
@ -400,7 +404,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>

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

@ -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,45 +1,49 @@
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(fallbackColor: Color = TangemColorPalette.Black): Color {
if (network.isTestnet) return fallbackColor
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

@ -50,6 +50,53 @@ sealed interface TextReference {
data class Combined(val refs: WrappedList<TextReference>) : TextReference
}
/**
* 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] */
@Composable
@ReadOnlyComposable

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

View file

@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors {
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(

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>