Updated on 2026-08-14
This commit is contained in:
commit
a98ecb4182
44 changed files with 551 additions and 24 deletions
|
|
@ -14,6 +14,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
|
|
@ -124,5 +125,7 @@ interface ApplicationEntryPoint {
|
||||||
|
|
||||||
fun getExcludedBlockchains(): ExcludedBlockchains
|
fun getExcludedBlockchains(): ExcludedBlockchains
|
||||||
|
|
||||||
|
fun getAppLogsStore(): AppLogsStore
|
||||||
|
|
||||||
fun getClipboardManager(): ClipboardManager
|
fun getClipboardManager(): ClipboardManager
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||||
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
|
|
@ -67,6 +68,7 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
import kotlin.collections.set
|
||||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
||||||
|
|
||||||
lateinit var store: Store<AppState>
|
lateinit var store: Store<AppState>
|
||||||
|
|
@ -195,6 +197,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
||||||
private val excludedBlockchains: ExcludedBlockchains
|
private val excludedBlockchains: ExcludedBlockchains
|
||||||
get() = entryPoint.getExcludedBlockchains()
|
get() = entryPoint.getExcludedBlockchains()
|
||||||
|
|
||||||
|
private val appLogsStore: AppLogsStore
|
||||||
|
get() = entryPoint.getAppLogsStore()
|
||||||
|
|
||||||
private val clipboardManager: ClipboardManager
|
private val clipboardManager: ClipboardManager
|
||||||
get() = entryPoint.getClipboardManager()
|
get() = entryPoint.getClipboardManager()
|
||||||
// endregion
|
// endregion
|
||||||
|
|
@ -203,6 +208,21 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
|
updateLogFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLogFiles() {
|
||||||
|
appLogsStore.deleteOldLogsFile()
|
||||||
|
appLogsStore.deleteLastLogFile()
|
||||||
|
|
||||||
|
// Temporally logs are not saved
|
||||||
|
// scope.launch {
|
||||||
|
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
|
||||||
|
// appLogsStore.deleteLastLogFile()
|
||||||
|
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("UnusedPrivateMember")
|
||||||
internal class TangemCardSDKLogger(
|
internal class TangemCardSDKLogger(
|
||||||
private val levels: List<Log.Level>,
|
private val levels: List<Log.Level>,
|
||||||
private val messageFormatter: LogFormat,
|
private val messageFormatter: LogFormat,
|
||||||
|
|
@ -21,8 +22,9 @@ internal class TangemCardSDKLogger(
|
||||||
) : TangemSdkLogger {
|
) : TangemSdkLogger {
|
||||||
|
|
||||||
override fun log(message: () -> String, level: Log.Level) {
|
override fun log(message: () -> String, level: Log.Level) {
|
||||||
if (!levels.contains(level)) return
|
// Disabled for now in [REDACTED_JIRA]
|
||||||
|
// if (!levels.contains(level)) return
|
||||||
appLogsStore.saveLogMessage(message = messageFormatter.format(message, level))
|
//
|
||||||
|
// appLogsStore.saveLogMessage(message = messageFormatter.format(message, level))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,4 +159,10 @@ internal object WalletsDomainModule {
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase {
|
||||||
|
return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -658,6 +658,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
||||||
|
|
||||||
var userWallet: UserWallet? = null
|
var userWallet: UserWallet? = null
|
||||||
if (scanResponse != null) {
|
if (scanResponse != null) {
|
||||||
|
scanResponse = updateScanResponseAfterBackup(scanResponse!!, backupState)
|
||||||
userWallet = createUserWallet(
|
userWallet = createUserWallet(
|
||||||
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
|
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
|
|
@ -691,6 +692,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
userWallet?.let {
|
||||||
|
if (it.scanResponse.cardTypesResolver.isWallet2() && it.isImported) {
|
||||||
|
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(it.walletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
|
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
|
||||||
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
|
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(projects.core.utils)
|
||||||
|
|
||||||
// region Firebase libraries
|
// region Firebase libraries
|
||||||
implementation(platform(deps.firebase.bom))
|
implementation(platform(deps.firebase.bom))
|
||||||
implementation(deps.firebase.analytics)
|
implementation(deps.firebase.analytics)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.tangem.common
|
||||||
|
|
||||||
|
import com.tangem.utils.SupportedLanguages
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
object TangemBlogUrlBuilder {
|
||||||
|
|
||||||
|
fun build(post: Post): String {
|
||||||
|
val code = SupportedLanguages.getCurrentSupportedLanguageCode()
|
||||||
|
.takeIf { code ->
|
||||||
|
code == SupportedLanguages.RUSSIAN || code == SupportedLanguages.ENGLISH
|
||||||
|
}
|
||||||
|
?: SupportedLanguages.ENGLISH
|
||||||
|
|
||||||
|
return "https://tangem.com/$code/blog/post/${post.path}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Post {
|
||||||
|
|
||||||
|
val path: String
|
||||||
|
|
||||||
|
data object SeedNotify : Post {
|
||||||
|
override val path: String = "seed-notify"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,9 @@ interface TangemTechApi {
|
||||||
@Body userTokens: UserTokensResponse,
|
@Body userTokens: UserTokensResponse,
|
||||||
): ApiResponse<Unit>
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
|
@POST("user-tokens")
|
||||||
|
suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse<Unit>
|
||||||
|
|
||||||
/** Returns referral status by [walletId] */
|
/** Returns referral status by [walletId] */
|
||||||
@GET("referral/{walletId}")
|
@GET("referral/{walletId}")
|
||||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse
|
suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse
|
||||||
|
|
@ -108,6 +111,17 @@ interface TangemTechApi {
|
||||||
@GET("networks/providers")
|
@GET("networks/providers")
|
||||||
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
|
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
|
||||||
|
|
||||||
|
@GET("seedphrase-notification/{wallet_id}")
|
||||||
|
suspend fun getSeedPhraseNotificationStatus(
|
||||||
|
@Path("wallet_id") walletId: String,
|
||||||
|
): ApiResponse<SeedPhraseNotificationDTO>
|
||||||
|
|
||||||
|
@PUT("seedphrase-notification/{wallet_id}")
|
||||||
|
suspend fun updateSeedPhraseNotificationStatus(
|
||||||
|
@Path("wallet_id") walletId: String,
|
||||||
|
@Body body: SeedPhraseNotificationDTO,
|
||||||
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val marketsQuoteFields = listOf(
|
val marketsQuoteFields = listOf(
|
||||||
"price",
|
"price",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.tangem.datasource.api.tangemTech.models
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class MarkUserWalletWasCreatedBody(
|
||||||
|
@Json(name = "user_wallet_id") val userWalletId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.tangem.datasource.api.tangemTech.models
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SeedPhraseNotificationDTO(val status: Status) {
|
||||||
|
|
||||||
|
enum class Status {
|
||||||
|
@Json(name = "notneeded")
|
||||||
|
NOT_NEEDED,
|
||||||
|
|
||||||
|
@Json(name = "notified")
|
||||||
|
NOTIFIED,
|
||||||
|
|
||||||
|
@Json(name = "declined")
|
||||||
|
DECLINED,
|
||||||
|
|
||||||
|
@Json(name = "confirmed")
|
||||||
|
CONFIRMED,
|
||||||
|
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ class AppLogsStore @Inject constructor(
|
||||||
)
|
)
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val file = File(applicationContext.filesDir, LOG_FILE_NAME)
|
private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME)
|
||||||
|
|
||||||
private val formatter = DateTimeFormatterBuilder()
|
private val formatter = DateTimeFormatterBuilder()
|
||||||
.appendDayOfMonth(2)
|
.appendDayOfMonth(2)
|
||||||
|
|
@ -59,6 +59,9 @@ class AppLogsStore @Inject constructor(
|
||||||
|
|
||||||
/** Save log [message] */
|
/** Save log [message] */
|
||||||
fun saveLogMessage(message: String) {
|
fun saveLogMessage(message: String) {
|
||||||
|
// Temporally logs are not saved
|
||||||
|
return
|
||||||
|
|
||||||
launchWithLock {
|
launchWithLock {
|
||||||
createFileIfNotExist()
|
createFileIfNotExist()
|
||||||
|
|
||||||
|
|
@ -68,6 +71,9 @@ class AppLogsStore @Inject constructor(
|
||||||
|
|
||||||
/** Save log that consists from [messages] */
|
/** Save log that consists from [messages] */
|
||||||
fun saveLogMessage(vararg messages: String) {
|
fun saveLogMessage(vararg messages: String) {
|
||||||
|
// Temporally logs are not saved
|
||||||
|
return
|
||||||
|
|
||||||
launchWithLock {
|
launchWithLock {
|
||||||
createFileIfNotExist()
|
createFileIfNotExist()
|
||||||
|
|
||||||
|
|
@ -84,6 +90,18 @@ class AppLogsStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteOldLogsFile() {
|
||||||
|
val file = File(applicationContext.filesDir, LOG_FILE_NAME)
|
||||||
|
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteLastLogFile() {
|
||||||
|
val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME)
|
||||||
|
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
private fun writeMessage(vararg messages: String) {
|
private fun writeMessage(vararg messages: String) {
|
||||||
BufferedWriter(FileWriter(file, true)).use { writer ->
|
BufferedWriter(FileWriter(file, true)).use { writer ->
|
||||||
writer.append(formatter.print(DateTime.now()))
|
writer.append(formatter.print(DateTime.now()))
|
||||||
|
|
@ -112,5 +130,6 @@ class AppLogsStore @Inject constructor(
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val LOG_FILE_NAME = "logs.txt"
|
const val LOG_FILE_NAME = "logs.txt"
|
||||||
|
const val NEW_LOG_FILE_NAME = "app_logs.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +111,8 @@ object PreferencesKeys {
|
||||||
|
|
||||||
val IS_GOOGLE_PAY_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGooglePayAvailable") }
|
val IS_GOOGLE_PAY_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGooglePayAvailable") }
|
||||||
|
|
||||||
|
val WAS_LOG_FILE_CLEARED by lazy { booleanPreferencesKey(name = "wasLogFileCleared") }
|
||||||
|
|
||||||
// region Permission
|
// region Permission
|
||||||
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
|
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
<string name="balance_hidden_do_not_show_button">Nicht mehr anzeigen</string>
|
<string name="balance_hidden_do_not_show_button">Nicht mehr anzeigen</string>
|
||||||
<string name="balance_hidden_got_it_button">Verstanden</string>
|
<string name="balance_hidden_got_it_button">Verstanden</string>
|
||||||
<string name="balance_hidden_title">Guthaben sind ausgeblendet</string>
|
<string name="balance_hidden_title">Guthaben sind ausgeblendet</string>
|
||||||
|
<string name="beta_mode_warning_message">Laut den Blockchain-Entwicklern befinden sich der Kaspa-Token derzeit in der Betaphase. Bleibe dran für Updates!</string>
|
||||||
|
<string name="beta_mode_warning_title">Beta-Phase</string>
|
||||||
<string name="biometric_lockout_permanent_warning_description">Bitte Karte oder Ring scannen</string>
|
<string name="biometric_lockout_permanent_warning_description">Bitte Karte oder Ring scannen</string>
|
||||||
<string name="biometric_lockout_warning_description">Bitte versuche es in 30 Sekunden erneut oder scanne die Karte oder Ring</string>
|
<string name="biometric_lockout_warning_description">Bitte versuche es in 30 Sekunden erneut oder scanne die Karte oder Ring</string>
|
||||||
<string name="biometric_lockout_warning_title">Zu viele Versuche</string>
|
<string name="biometric_lockout_warning_title">Zu viele Versuche</string>
|
||||||
|
|
@ -1065,6 +1067,10 @@
|
||||||
<string name="warning_rate_app_title">Gefällt dir Tangem?</string>
|
<string name="warning_rate_app_title">Gefällt dir Tangem?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">Du musst deinen Token zuordnen, bevor du Token erhalten kannst</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">Du musst deinen Token zuordnen, bevor du Token erhalten kannst</string>
|
||||||
<string name="warning_rent_fee_title">Netzmietgebühr erforderlich</string>
|
<string name="warning_rent_fee_title">Netzmietgebühr erforderlich</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">Vielen Dank! Alles erledigt! Keine weiteren Maßnahmen erforderlich.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">Sie werden jetzt auf die offizielle Tangem-Website weitergeleitet. Bitte lesen und befolgen Sie dort die Anweisungen.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">Haben Sie jemals das Support-Team von Tangem direkt über diese Anwendung kontaktiert?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Verpflichtendes Sicherheitsupdate</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s ist ein Vermögenswert im %2$s Netzwerk. Um eine %3$s Transaktion durchzuführen, musst du etwas %4$s (%5$s) einzahlen, um die Netzwerkgebühr zu decken.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s ist ein Vermögenswert im %2$s Netzwerk. Um eine %3$s Transaktion durchzuführen, musst du etwas %4$s (%5$s) einzahlen, um die Netzwerkgebühr zu decken.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">Unzureichende %1$s zur Deckung der Netzgebühr</string>
|
<string name="warning_send_blocked_funds_for_fee_title">Unzureichende %1$s zur Deckung der Netzgebühr</string>
|
||||||
<string name="warning_solana_fee_message">Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion.</string>
|
<string name="warning_solana_fee_message">Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion.</string>
|
||||||
|
|
|
||||||
|
|
@ -1065,6 +1065,10 @@
|
||||||
<string name="warning_rate_app_title">¿Disfrutando de Tangem?</string>
|
<string name="warning_rate_app_title">¿Disfrutando de Tangem?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">Deba asociar su token antes de recibir tokens</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">Deba asociar su token antes de recibir tokens</string>
|
||||||
<string name="warning_rent_fee_title">Se requiere tarifa de alquiler de red</string>
|
<string name="warning_rent_fee_title">Se requiere tarifa de alquiler de red</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">¡Gracias! ¡Todo está listo! No se requieren más acciones.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">Ahora será redirigido al sitio web oficial de Tangem. Por favor, lea y siga las instrucciones allí indicadas.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">¿Alguna vez contactó al equipo de soporte de Tangem directamente desde esta aplicación?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Actualización de seguridad obligatoria</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s un activo en la red %2$s. Para realizar una transacción %3$s, deposite %4$s (%5$s) para cubrir la tarifa de red.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s un activo en la red %2$s. Para realizar una transacción %3$s, deposite %4$s (%5$s) para cubrir la tarifa de red.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuficiente para cubrir la tarifa de red</string>
|
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuficiente para cubrir la tarifa de red</string>
|
||||||
<string name="warning_solana_fee_message">La red Solana está congestionada. Si su transacción no se procesa dentro de 2 minutos, repita la transacción.</string>
|
<string name="warning_solana_fee_message">La red Solana está congestionada. Si su transacción no se procesa dentro de 2 minutos, repita la transacción.</string>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
<string name="balance_hidden_do_not_show_button">Ne plus afficher</string>
|
<string name="balance_hidden_do_not_show_button">Ne plus afficher</string>
|
||||||
<string name="balance_hidden_got_it_button">Compris</string>
|
<string name="balance_hidden_got_it_button">Compris</string>
|
||||||
<string name="balance_hidden_title">Les soldes sont masqués</string>
|
<string name="balance_hidden_title">Les soldes sont masqués</string>
|
||||||
|
<string name="beta_mode_warning_message">Selon les développeurs de la blockchain, les jetons Kaspa sont actuellement en version bêta. Restez à l\'écoute des mises à jour !</string>
|
||||||
|
<string name="beta_mode_warning_title">Mode bêta</string>
|
||||||
<string name="biometric_lockout_permanent_warning_description">Veuillez scanner la carte/bague</string>
|
<string name="biometric_lockout_permanent_warning_description">Veuillez scanner la carte/bague</string>
|
||||||
<string name="biometric_lockout_warning_description">Veuillez réessayer dans 30 secondes ou scannez la carte/bague</string>
|
<string name="biometric_lockout_warning_description">Veuillez réessayer dans 30 secondes ou scannez la carte/bague</string>
|
||||||
<string name="biometric_lockout_warning_title">Trop de tentatives</string>
|
<string name="biometric_lockout_warning_title">Trop de tentatives</string>
|
||||||
|
|
@ -1065,6 +1067,10 @@
|
||||||
<string name="warning_rate_app_title">Vous appréciez Tangem ?</string>
|
<string name="warning_rate_app_title">Vous appréciez Tangem ?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">Vous devez associer votre jeton avant de recevoir des jetons</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">Vous devez associer votre jeton avant de recevoir des jetons</string>
|
||||||
<string name="warning_rent_fee_title">Frais de location de réseau requis</string>
|
<string name="warning_rent_fee_title">Frais de location de réseau requis</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">Merci ! Tout est en ordre ! Aucune autre action n\'est requise.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">Vous allez être redirigé vers le site officiel de Tangem. Veuillez lire et suivre les instructions qui y sont indiquées.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">Avez-vous déjà contacté l\'équipe de support Tangem directement depuis cette application ?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Mise à jour de sécurité obligatoire</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s s\'agit d\'un actif du réseau %2$s. Pour effectuer une transaction %3$s, vous devez déposer une certaine quantité de %4$s (%5$s) pour couvrir les frais de réseau.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s s\'agit d\'un actif du réseau %2$s. Pour effectuer une transaction %3$s, vous devez déposer une certaine quantité de %4$s (%5$s) pour couvrir les frais de réseau.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuffisant pour couvrir les frais de réseau</string>
|
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuffisant pour couvrir les frais de réseau</string>
|
||||||
<string name="warning_solana_fee_message">Le réseau Solana est encombré. Si votre transaction n\'est pas traitée dans les 2 minutes, veuillez répéter la transaction.</string>
|
<string name="warning_solana_fee_message">Le réseau Solana est encombré. Si votre transaction n\'est pas traitée dans les 2 minutes, veuillez répéter la transaction.</string>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
<string name="balance_hidden_do_not_show_button">今後表示しない</string>
|
<string name="balance_hidden_do_not_show_button">今後表示しない</string>
|
||||||
<string name="balance_hidden_got_it_button">わかりました</string>
|
<string name="balance_hidden_got_it_button">わかりました</string>
|
||||||
<string name="balance_hidden_title">残高は非表示</string>
|
<string name="balance_hidden_title">残高は非表示</string>
|
||||||
|
<string name="beta_mode_warning_message">ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに!</string>
|
||||||
|
<string name="beta_mode_warning_title">ベータモード</string>
|
||||||
<string name="biometric_lockout_permanent_warning_description">カードまたはリングをスキャンしてください</string>
|
<string name="biometric_lockout_permanent_warning_description">カードまたはリングをスキャンしてください</string>
|
||||||
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
|
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
|
||||||
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>
|
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>
|
||||||
|
|
|
||||||
|
|
@ -1086,6 +1086,10 @@
|
||||||
<string name="warning_rate_app_title">Нравится Tangem?</string>
|
<string name="warning_rate_app_title">Нравится Tangem?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его</string>
|
||||||
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
|
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">Спасибо! Всё в порядке! Дальнейшие действия не требуются.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">Вы будете перенаправлены на официальный сайт Tangem. Пожалуйста, прочитайте и выполните указанные там инструкции.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">Вы когда-либо обращались в службу поддержки Tangem напрямую из этого приложения?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Обязательное обновление безопасности</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
|
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
|
||||||
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>
|
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>
|
||||||
|
|
|
||||||
|
|
@ -1087,6 +1087,10 @@
|
||||||
<string name="warning_rate_app_title">Подобається Tangem?</string>
|
<string name="warning_rate_app_title">Подобається Tangem?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необхідно провести асоціацію токена, щоб мати можливість приймати його</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необхідно провести асоціацію токена, щоб мати можливість приймати його</string>
|
||||||
<string name="warning_rent_fee_title">Необхідна плата за оренду мережі</string>
|
<string name="warning_rent_fee_title">Необхідна плата за оренду мережі</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">Дякуємо! Все добре! Подальших дій не потрібно.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">Ви будете перенаправлені на офіційний сайт Tangem. Будь ласка, прочитайте та виконайте вказані там інструкції.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">Ви коли-небудь зверталися до служби підтримки Tangem безпосередньо з цього додатку?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Обов’язкове оновлення безпеки</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s є активом у мережі %2$s. Щоб здійснити транзакцію %3$s, ви повинні внести певну суму %4$s (%5$s), щоб покрити комісію мережі.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s є активом у мережі %2$s. Щоб здійснити транзакцію %3$s, ви повинні внести певну суму %4$s (%5$s), щоб покрити комісію мережі.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">Недостатньо %1$s для покриття комісії мережі</string>
|
<string name="warning_send_blocked_funds_for_fee_title">Недостатньо %1$s для покриття комісії мережі</string>
|
||||||
<string name="warning_solana_fee_message">Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію.</string>
|
<string name="warning_solana_fee_message">Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію.</string>
|
||||||
|
|
|
||||||
|
|
@ -1067,6 +1067,10 @@
|
||||||
<string name="warning_rate_app_title">Enjoying Tangem?</string>
|
<string name="warning_rate_app_title">Enjoying Tangem?</string>
|
||||||
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
|
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
|
||||||
<string name="warning_rent_fee_title">Network rent fee required</string>
|
<string name="warning_rent_fee_title">Network rent fee required</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_no">Thank you! All set! No further actions required.</string>
|
||||||
|
<string name="warning_seedphrase_issue_answer_yes">You will now be redirected to the official Tangem website. Please read and follow instructions there.</string>
|
||||||
|
<string name="warning_seedphrase_issue_message">Have you ever contacted the Tangem support team directly through this application?</string>
|
||||||
|
<string name="warning_seedphrase_issue_title">Mandatory security update</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_message">%1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.</string>
|
<string name="warning_send_blocked_funds_for_fee_message">%1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.</string>
|
||||||
<string name="warning_send_blocked_funds_for_fee_title">Insufficient %1$s to cover network fee</string>
|
<string name="warning_send_blocked_funds_for_fee_title">Insufficient %1$s to cover network fee</string>
|
||||||
<string name="warning_solana_fee_message">The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.</string>
|
<string name="warning_solana_fee_message">The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.</string>
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,10 @@ private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true)
|
||||||
)
|
)
|
||||||
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled)
|
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled)
|
||||||
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled)
|
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled)
|
||||||
|
is NotificationConfig.ButtonsState.SecondaryPairButtonsConfig -> SecondaryPairButtons(
|
||||||
|
config = state,
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
)
|
||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +288,30 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SecondaryPairButtons(
|
||||||
|
config: NotificationButtonsState.SecondaryPairButtonsConfig,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
|
||||||
|
SecondaryButton(
|
||||||
|
text = config.leftText.resolveReference(),
|
||||||
|
onClick = config.onLeftClick,
|
||||||
|
modifier = Modifier.weight(weight = 1f),
|
||||||
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryButton(
|
||||||
|
text = config.rightText.resolveReference(),
|
||||||
|
onClick = config.onRightClick,
|
||||||
|
modifier = Modifier.weight(weight = 1f),
|
||||||
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CloseableIconButton(
|
internal fun CloseableIconButton(
|
||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
|
|
@ -385,6 +413,17 @@ private class NotificationConfigProvider : CollectionPreviewParameterProvider<No
|
||||||
onSecondaryClick = {},
|
onSecondaryClick = {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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.SecondaryPairButtonsConfig(
|
||||||
|
leftText = TextReference.Str(value = "Love it!"),
|
||||||
|
onLeftClick = {},
|
||||||
|
rightText = TextReference.Str(value = "Can be better"),
|
||||||
|
onRightClick = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
NotificationConfig(
|
NotificationConfig(
|
||||||
title = TextReference.Str(value = "Note top up"),
|
title = TextReference.Str(value = "Note top up"),
|
||||||
subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"),
|
subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"),
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,12 @@ data class NotificationConfig(
|
||||||
val secondaryText: TextReference,
|
val secondaryText: TextReference,
|
||||||
val onSecondaryClick: () -> Unit,
|
val onSecondaryClick: () -> Unit,
|
||||||
) : ButtonsState()
|
) : ButtonsState()
|
||||||
|
|
||||||
|
data class SecondaryPairButtonsConfig(
|
||||||
|
val leftText: TextReference,
|
||||||
|
val onLeftClick: () -> Unit,
|
||||||
|
val rightText: TextReference,
|
||||||
|
val onRightClick: () -> Unit,
|
||||||
|
) : ButtonsState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
package com.tangem.data.wallets
|
package com.tangem.data.wallets
|
||||||
|
|
||||||
|
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
||||||
|
import com.tangem.datasource.api.common.response.getOrThrow
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.datasource.local.preferences.utils.get
|
import com.tangem.datasource.local.preferences.utils.get
|
||||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||||
import com.tangem.datasource.local.preferences.utils.store
|
import com.tangem.datasource.local.preferences.utils.store
|
||||||
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.coroutines.runCatching
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal class DefaultWalletsRepository(
|
internal class DefaultWalletsRepository(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val tangemTechApi: TangemTechApi,
|
||||||
|
private val userWalletsStore: UserWalletsStore,
|
||||||
|
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<Boolean>,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : WalletsRepository {
|
) : WalletsRepository {
|
||||||
|
|
||||||
override suspend fun shouldSaveUserWalletsSync(): Boolean {
|
override suspend fun shouldSaveUserWalletsSync(): Boolean {
|
||||||
|
|
@ -38,4 +55,71 @@ internal class DefaultWalletsRepository(
|
||||||
it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue
|
it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean> {
|
||||||
|
return channelFlow {
|
||||||
|
launch {
|
||||||
|
seedPhraseNotificationVisibilityStore.get().collectLatest(::send)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSeedPhraseNotificationStatus(userWalletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchSeedPhraseNotificationStatus(userWalletId: UserWalletId) {
|
||||||
|
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
|
||||||
|
|
||||||
|
val status = if (userWallet?.isImported == false) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
runCatching(dispatchers.io) {
|
||||||
|
tangemTechApi.getSeedPhraseNotificationStatus(walletId = userWalletId.stringValue).getOrThrow()
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it.status == Status.NOTIFIED },
|
||||||
|
onFailure = { it is HttpException && it.code == HttpException.Code.NOT_FOUND },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhraseNotificationVisibilityStore.store(value = status)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) {
|
||||||
|
runCatching(dispatchers.io) {
|
||||||
|
tangemTechApi.updateSeedPhraseNotificationStatus(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
body = SeedPhraseNotificationDTO(status = Status.NOTIFIED),
|
||||||
|
).getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId) {
|
||||||
|
runCatching(dispatchers.io) {
|
||||||
|
tangemTechApi.updateSeedPhraseNotificationStatus(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
body = SeedPhraseNotificationDTO(status = Status.CONFIRMED),
|
||||||
|
).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhraseNotificationVisibilityStore.store(value = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) {
|
||||||
|
runCatching(dispatchers.io) {
|
||||||
|
tangemTechApi.updateSeedPhraseNotificationStatus(
|
||||||
|
walletId = userWalletId.stringValue,
|
||||||
|
body = SeedPhraseNotificationDTO(status = Status.DECLINED),
|
||||||
|
).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhraseNotificationVisibilityStore.store(value = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markWallet2WasCreated(userWalletId: UserWalletId) {
|
||||||
|
runCatching(dispatchers.io) {
|
||||||
|
tangemTechApi.markUserWallerWasCreated(
|
||||||
|
body = MarkUserWalletWasCreatedBody(userWalletId = userWalletId.stringValue),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,13 @@ package com.tangem.data.wallets.di
|
||||||
|
|
||||||
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
||||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||||
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -17,8 +21,19 @@ internal object WalletsDataModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesWalletsRepository(appPreferencesStore: AppPreferencesStore): WalletsRepository {
|
fun providesWalletsRepository(
|
||||||
return DefaultWalletsRepository(appPreferencesStore = appPreferencesStore)
|
appPreferencesStore: AppPreferencesStore,
|
||||||
|
tangemTechApi: TangemTechApi,
|
||||||
|
userWalletsStore: UserWalletsStore,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): WalletsRepository {
|
||||||
|
return DefaultWalletsRepository(
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
tangemTechApi = tangemTechApi,
|
||||||
|
userWalletsStore = userWalletsStore,
|
||||||
|
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = false),
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ class SendFeedbackEmailUseCase(
|
||||||
address = getAddress(type.cardInfo),
|
address = getAddress(type.cardInfo),
|
||||||
subject = emailSubjectResolver.resolve(type),
|
subject = emailSubjectResolver.resolve(type),
|
||||||
message = createMessage(type),
|
message = createMessage(type),
|
||||||
file = feedbackRepository.getLogFile(),
|
// Temporally user data is not sent
|
||||||
|
file = null, // feedbackRepository.getLogFile(),
|
||||||
)
|
)
|
||||||
|
|
||||||
feedbackRepository.sendEmail(email)
|
feedbackRepository.sendEmail(email)
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,14 @@ interface WalletsRepository {
|
||||||
suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean
|
suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean
|
||||||
|
|
||||||
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)
|
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)
|
||||||
|
|
||||||
|
fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId)
|
||||||
|
|
||||||
|
suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId)
|
||||||
|
|
||||||
|
suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId)
|
||||||
|
|
||||||
|
suspend fun markWallet2WasCreated(userWalletId: UserWalletId)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.tangem.domain.wallets.usecase
|
||||||
|
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class SeedPhraseNotificationUseCase(
|
||||||
|
private val walletsRepository: WalletsRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(userWalletId: UserWalletId): Flow<Boolean> {
|
||||||
|
return walletsRepository.seedPhraseNotificationStatus(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun notified(userWalletId: UserWalletId) {
|
||||||
|
walletsRepository.notifiedSeedPhraseNotification(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun confirm(userWalletId: UserWalletId) {
|
||||||
|
walletsRepository.confirmSeedPhraseNotification(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun decline(userWalletId: UserWalletId) {
|
||||||
|
walletsRepository.declineSeedPhraseNotification(userWalletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -640,6 +640,7 @@ internal class StakingViewModel @Inject constructor(
|
||||||
amount = amount.orZero(),
|
amount = amount.orZero(),
|
||||||
fee = fee.orZero(),
|
fee = fee.orZero(),
|
||||||
reduceAmountBy = confirmationState?.reduceAmountBy.orZero(),
|
reduceAmountBy = confirmationState?.reduceAmountBy.orZero(),
|
||||||
|
actionType = value.actionType,
|
||||||
)
|
)
|
||||||
val currencyStatus = getCurrencyCheckUseCase(
|
val currencyStatus = getCurrencyCheckUseCase(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
|
|
@ -699,14 +700,21 @@ internal class StakingViewModel @Inject constructor(
|
||||||
amount: BigDecimal,
|
amount: BigDecimal,
|
||||||
fee: BigDecimal,
|
fee: BigDecimal,
|
||||||
reduceAmountBy: BigDecimal,
|
reduceAmountBy: BigDecimal,
|
||||||
|
actionType: StakingActionCommonType,
|
||||||
): BigDecimal? {
|
): BigDecimal? {
|
||||||
val subtractedBalanceAmount = checkAndCalculateSubtractedAmount(
|
// TODO split for different networks
|
||||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
val subtractedBalanceAmount = when (actionType) {
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
StakingActionCommonType.Enter -> checkAndCalculateSubtractedAmount(
|
||||||
amountValue = amount.orZero(),
|
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||||
feeValue = fee.orZero(),
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
reduceAmountBy = reduceAmountBy,
|
amountValue = amount.orZero(),
|
||||||
)
|
feeValue = fee.orZero(),
|
||||||
|
reduceAmountBy = reduceAmountBy,
|
||||||
|
)
|
||||||
|
StakingActionCommonType.Exit,
|
||||||
|
is StakingActionCommonType.Pending,
|
||||||
|
-> BigDecimal.ZERO
|
||||||
|
}
|
||||||
val statusValue = cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded
|
val statusValue = cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded
|
||||||
return statusValue?.let { it.amount - subtractedBalanceAmount - fee.orZero() }
|
return statusValue?.let { it.amount - subtractedBalanceAmount - fee.orZero() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,9 @@ dependencies {
|
||||||
implementation(projects.features.onboardingV2.api)
|
implementation(projects.features.onboardingV2.api)
|
||||||
|
|
||||||
/** Common modules */
|
/** Common modules */
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common)
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
implementation(projects.common.ui)
|
||||||
|
|
||||||
/** Test libraries */
|
/** Test libraries */
|
||||||
implementation(deps.test.junit)
|
implementation(deps.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -126,5 +126,11 @@ sealed class WalletScreenAnalyticsEvent {
|
||||||
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
||||||
|
|
||||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||||
|
|
||||||
|
data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support")
|
||||||
|
|
||||||
|
data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No")
|
||||||
|
|
||||||
|
data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +62,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
||||||
source = AnalyticsParam.ScreensSources.Main,
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Ring,
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Ring,
|
||||||
)
|
)
|
||||||
|
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
||||||
|
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||||
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ViewModelScoped
|
||||||
|
internal class WalletWarningsSingleEventSender @Inject constructor(
|
||||||
|
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
|
||||||
|
private val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun send(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
displayedUiState: WalletState?,
|
||||||
|
newWarnings: List<WalletNotification>,
|
||||||
|
) {
|
||||||
|
if (screenLifecycleProvider.isBackgroundState.value) return
|
||||||
|
if (newWarnings.isEmpty()) return
|
||||||
|
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
|
||||||
|
|
||||||
|
val events = newWarnings.filter { it !in displayedUiState.warnings }
|
||||||
|
|
||||||
|
events.forEach { event ->
|
||||||
|
if (event is WalletNotification.Critical.SeedPhraseNotification) {
|
||||||
|
seedPhraseNotificationUseCase.notified(userWalletId = userWalletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.tokens.model.TokenList
|
import com.tangem.domain.tokens.model.TokenList
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
|
@ -35,23 +36,33 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
private val promoRepository: PromoRepository,
|
private val promoRepository: PromoRepository,
|
||||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||||
private val backupValidator: BackupValidator,
|
private val backupValidator: BackupValidator,
|
||||||
|
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Suppress("MagicNumber", "MaximumLineLength")
|
||||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
|
|
||||||
val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) }
|
val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) }
|
||||||
return combine(
|
return combine(
|
||||||
flow = tokenListStore.getOrThrow(userWallet.walletId),
|
tokenListStore.getOrThrow(userWallet.walletId),
|
||||||
flow2 = isReadyToShowRateAppUseCase(),
|
isReadyToShowRateAppUseCase(),
|
||||||
flow3 = isNeedToBackupUseCase(userWallet.walletId),
|
isNeedToBackupUseCase(userWallet.walletId),
|
||||||
flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
|
shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
|
||||||
flow5 = promoFlow,
|
promoFlow,
|
||||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
|
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
|
||||||
|
) {
|
||||||
|
val maybeTokenList = it[0] as Lce<TokenListError, TokenList>
|
||||||
|
val isReadyToShowRating = it[1] as Boolean
|
||||||
|
val isNeedToBackup = it[2] as Boolean
|
||||||
|
val shouldShowPromo = it[3] as Boolean
|
||||||
|
val promoBanner = it[4] as? PromoBanner
|
||||||
|
val seedPhraseIssueStatus = it[5] as Boolean
|
||||||
|
|
||||||
buildList {
|
buildList {
|
||||||
addRingPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
addRingPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||||
|
|
||||||
addCriticalNotifications(userWallet, clickIntents)
|
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
|
||||||
|
|
||||||
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
||||||
|
|
||||||
|
|
@ -83,8 +94,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
|
|
||||||
private fun MutableList<WalletNotification>.addCriticalNotifications(
|
private fun MutableList<WalletNotification>.addCriticalNotifications(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
|
seedPhraseIssueStatus: Boolean,
|
||||||
clickIntents: WalletClickIntents,
|
clickIntents: WalletClickIntents,
|
||||||
) {
|
) {
|
||||||
|
addIf(
|
||||||
|
element = WalletNotification.Critical.SeedPhraseNotification(
|
||||||
|
onDeclineClick = clickIntents::onSeedPhraseNotificationDecline,
|
||||||
|
onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm,
|
||||||
|
),
|
||||||
|
condition = with(userWallet) {
|
||||||
|
val isDemo = isDemoCardUseCase(cardId = userWallet.cardId)
|
||||||
|
val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported
|
||||||
|
|
||||||
|
!isDemo && isWalletWithSeedPhrase && seedPhraseIssueStatus
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
addIf(
|
addIf(
|
||||||
element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() },
|
element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() },
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||||
|
|
@ -22,6 +23,7 @@ internal class MultiWalletContentLoader(
|
||||||
private val clickIntents: WalletClickIntents,
|
private val clickIntents: WalletClickIntents,
|
||||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||||
|
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||||
private val tokenListStore: MultiWalletTokenListStore,
|
private val tokenListStore: MultiWalletTokenListStore,
|
||||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
|
|
@ -49,6 +51,7 @@ internal class MultiWalletContentLoader(
|
||||||
clickIntents = clickIntents,
|
clickIntents = clickIntents,
|
||||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||||
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
||||||
|
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||||
|
|
@ -25,6 +26,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
|
||||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
||||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||||
|
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||||
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
|
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
|
||||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||||
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
||||||
|
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
|
||||||
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
|
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
|
||||||
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
|
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||||
|
|
@ -21,6 +22,7 @@ internal class SingleWalletWithTokenContentLoader(
|
||||||
private val stateHolder: WalletStateController,
|
private val stateHolder: WalletStateController,
|
||||||
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
|
||||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||||
|
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||||
private val walletWithFundsChecker: WalletWithFundsChecker,
|
private val walletWithFundsChecker: WalletWithFundsChecker,
|
||||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||||
private val tokenListStore: MultiWalletTokenListStore,
|
private val tokenListStore: MultiWalletTokenListStore,
|
||||||
|
|
@ -46,6 +48,7 @@ internal class SingleWalletWithTokenContentLoader(
|
||||||
clickIntents = clickIntents,
|
clickIntents = clickIntents,
|
||||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||||
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
||||||
|
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
|
||||||
|
|
@ -22,6 +23,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
|
||||||
private val tokenListStore: MultiWalletTokenListStore,
|
private val tokenListStore: MultiWalletTokenListStore,
|
||||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||||
|
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||||
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
|
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
|
||||||
tokenListStore = tokenListStore,
|
tokenListStore = tokenListStore,
|
||||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||||
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
||||||
|
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
|
||||||
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
|
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ internal sealed interface WalletAlertState {
|
||||||
abstract val errorTextProvider: (String) -> TextReference?
|
abstract val errorTextProvider: (String) -> TextReference?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SimpleOkAlert(val message: TextReference, val onOkClick: () -> Unit) : WalletAlertState
|
||||||
|
|
||||||
data class DefaultAlert(
|
data class DefaultAlert(
|
||||||
override val title: TextReference?,
|
override val title: TextReference?,
|
||||||
override val message: TextReference,
|
override val message: TextReference,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
import com.tangem.core.ui.extensions.*
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.pluralReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
@ -47,6 +50,17 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
||||||
onClick = onSupportClick,
|
onClick = onSupportClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SeedPhraseNotification(val onDeclineClick: () -> Unit, val onConfirmClick: () -> Unit) : Critical(
|
||||||
|
title = resourceReference(R.string.warning_seedphrase_issue_title),
|
||||||
|
subtitle = resourceReference(R.string.warning_seedphrase_issue_message),
|
||||||
|
buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig(
|
||||||
|
leftText = resourceReference(R.string.common_no),
|
||||||
|
onLeftClick = onDeclineClick,
|
||||||
|
rightText = resourceReference(R.string.common_yes),
|
||||||
|
onRightClick = onConfirmClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Warning(
|
sealed class Warning(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ internal class AddWalletTransformer(
|
||||||
private val clickIntents: WalletClickIntents,
|
private val clickIntents: WalletClickIntents,
|
||||||
private val walletImageResolver: WalletImageResolver,
|
private val walletImageResolver: WalletImageResolver,
|
||||||
private val walletFeatureToggles: WalletFeatureToggles,
|
private val walletFeatureToggles: WalletFeatureToggles,
|
||||||
|
private val selectedWalletIndex: Int,
|
||||||
) : WalletScreenStateTransformer {
|
) : WalletScreenStateTransformer {
|
||||||
|
|
||||||
private val walletLoadingStateFactory by lazy {
|
private val walletLoadingStateFactory by lazy {
|
||||||
|
|
@ -26,6 +27,7 @@ internal class AddWalletTransformer(
|
||||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||||
return prevState.copy(
|
return prevState.copy(
|
||||||
wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(),
|
wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(),
|
||||||
|
selectedWalletIndex = selectedWalletIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||||
|
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
|
|
@ -20,6 +21,7 @@ internal class MultiWalletWarningsSubscriber(
|
||||||
private val clickIntents: WalletClickIntents,
|
private val clickIntents: WalletClickIntents,
|
||||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||||
|
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||||
) : WalletSubscriber() {
|
) : WalletSubscriber() {
|
||||||
|
|
||||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||||
|
|
@ -31,6 +33,11 @@ internal class MultiWalletWarningsSubscriber(
|
||||||
|
|
||||||
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
||||||
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
||||||
|
walletWarningsSingleEventSender.send(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
displayedUiState = displayedState,
|
||||||
|
newWarnings = warnings,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,19 @@ internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is WalletAlertState.Basic -> BasicAlert(state, onDismiss)
|
is WalletAlertState.Basic -> BasicAlert(state, onDismiss)
|
||||||
is WalletAlertState.TextInput -> TextInputAlert(state, onDismiss)
|
is WalletAlertState.TextInput -> TextInputAlert(state, onDismiss)
|
||||||
|
is WalletAlertState.SimpleOkAlert -> {
|
||||||
|
BasicDialog(
|
||||||
|
message = state.message.resolveReference(),
|
||||||
|
confirmButton = DialogButtonUM(
|
||||||
|
onClick = {
|
||||||
|
state.onOkClick()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onDismissDialog = onDismiss,
|
||||||
|
isDismissable = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,7 @@ internal class WalletViewModel @Inject constructor(
|
||||||
clickIntents = clickIntents,
|
clickIntents = clickIntents,
|
||||||
walletImageResolver = walletImageResolver,
|
walletImageResolver = walletImageResolver,
|
||||||
walletFeatureToggles = walletFeatureToggles,
|
walletFeatureToggles = walletFeatureToggles,
|
||||||
|
selectedWalletIndex = action.selectedWalletIndex,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||||
|
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
|
import com.tangem.common.TangemBlogUrlBuilder
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||||
import com.tangem.domain.card.SetCardWasScannedUseCase
|
import com.tangem.domain.card.SetCardWasScannedUseCase
|
||||||
|
|
@ -23,6 +25,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy
|
||||||
import com.tangem.domain.wallets.models.UnlockWalletsError
|
import com.tangem.domain.wallets.models.UnlockWalletsError
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
|
||||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
||||||
|
|
@ -68,6 +71,10 @@ internal interface WalletWarningsClickIntents {
|
||||||
fun onSupportClick()
|
fun onSupportClick()
|
||||||
|
|
||||||
fun onNoteMigrationButtonClick(url: String)
|
fun onNoteMigrationButtonClick(url: String)
|
||||||
|
|
||||||
|
fun onSeedPhraseNotificationConfirm()
|
||||||
|
|
||||||
|
fun onSeedPhraseNotificationDecline()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -90,6 +97,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase,
|
private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase,
|
||||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
|
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
|
||||||
|
private val urlOpener: UrlOpener,
|
||||||
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
||||||
|
|
||||||
override fun onAddBackupCardClick() {
|
override fun onAddBackupCardClick() {
|
||||||
|
|
@ -279,6 +288,48 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSeedPhraseNotificationConfirm() {
|
||||||
|
val userWallet = getSelectedUserWallet() ?: return
|
||||||
|
|
||||||
|
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes)
|
||||||
|
|
||||||
|
walletEventSender.send(
|
||||||
|
event = WalletEvent.ShowAlert(
|
||||||
|
state = WalletAlertState.SimpleOkAlert(
|
||||||
|
message = resourceReference(R.string.warning_seedphrase_issue_answer_yes),
|
||||||
|
onOkClick = {
|
||||||
|
viewModelScope.launch {
|
||||||
|
seedPhraseNotificationUseCase.confirm(userWalletId = userWallet.walletId)
|
||||||
|
|
||||||
|
urlOpener.openUrl(
|
||||||
|
url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotify),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSeedPhraseNotificationDecline() {
|
||||||
|
val userWallet = getSelectedUserWallet() ?: return
|
||||||
|
|
||||||
|
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo)
|
||||||
|
|
||||||
|
walletEventSender.send(
|
||||||
|
event = WalletEvent.ShowAlert(
|
||||||
|
state = WalletAlertState.SimpleOkAlert(
|
||||||
|
message = resourceReference(R.string.warning_seedphrase_issue_answer_no),
|
||||||
|
onOkClick = {
|
||||||
|
viewModelScope.launch {
|
||||||
|
seedPhraseNotificationUseCase.decline(userWalletId = userWallet.walletId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSelectedUserWallet(): UserWallet? {
|
private fun getSelectedUserWallet(): UserWallet? {
|
||||||
val userWalletId = stateHolder.getSelectedWalletId()
|
val userWalletId = stateHolder.getSelectedWalletId()
|
||||||
return getUserWalletUseCase(userWalletId).getOrElse {
|
return getUserWalletUseCase(userWalletId).getOrElse {
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ markdownComposeView = "0.5.4"
|
||||||
# region Tangem
|
# region Tangem
|
||||||
tangemBlockchainSdk = "develop-896"
|
tangemBlockchainSdk = "develop-896"
|
||||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||||
tangemCardSdk = "develop-416"
|
tangemCardSdk = "develop-420"
|
||||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||||
tangemVico = "2.0.0-alpha.25-tangem17"
|
tangemVico = "2.0.0-alpha.25-tangem17"
|
||||||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue