Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-24 17:36:59 +03:00
commit fbb99bcda0
317 changed files with 3800 additions and 2894 deletions

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common
import android.view.View
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.ui.extensions.TextReference
@ -11,14 +10,14 @@ interface SnackbarHandler {
@StringRes text: Int,
length: Int = Snackbar.LENGTH_INDEFINITE,
@StringRes buttonTitle: Int? = null,
action: View.OnClickListener? = null,
action: (() -> Unit)? = null,
)
fun showSnackbar(
text: TextReference,
length: Int = Snackbar.LENGTH_INDEFINITE,
buttonTitle: TextReference? = null,
action: View.OnClickListener? = null,
action: (() -> Unit)? = null,
)
fun dismissSnackbar()

View file

@ -0,0 +1,33 @@
package com.tangem.tap.common.analytics.handlers.firebase
internal class FirebaseAnalyticsEventConverter {
fun convertEventName(event: String): String {
return convertString(event, FIREBASE_EVENT_NAME_MAX_LENGTH)
}
fun convertEventParams(params: Map<String, String>): Map<String, String> {
return params.map { (key, value) ->
convertString(key, FIREBASE_EVENT_NAME_MAX_LENGTH) to convertString(value, FIREBASE_EVENT_VALUE_MAX_LENGTH)
}.toMap()
}
private fun convertString(string: String, maxLength: Int): String {
return string
.replace(REPLACING_PATTERN.toRegex(), WORD_SEPARATOR)
.trim { it in TRIMMING_CHARACTERS }
.trimToLength(maxLength)
}
private fun String.trimToLength(length: Int): String {
return if (this.length > length) this.substring(0, length) else this
}
private companion object {
const val REPLACING_PATTERN = "[^\\w]+"
const val WORD_SEPARATOR = "_"
const val TRIMMING_CHARACTERS = WORD_SEPARATOR
const val FIREBASE_EVENT_NAME_MAX_LENGTH = 40
const val FIREBASE_EVENT_VALUE_MAX_LENGTH = 100
}
}

View file

@ -18,12 +18,19 @@ internal class FirebaseClient : FirebaseAnalyticsClient {
private val fbAnalytics = Firebase.analytics
private val fbCrashlytics = Firebase.crashlytics
private val eventConverter = FirebaseAnalyticsEventConverter()
override fun logEvent(event: String, params: Map<String, String>) {
fbAnalytics.logEvent(event, params.toBundle())
fbAnalytics.logEvent(
eventConverter.convertEventName(event),
eventConverter.convertEventParams(params).toBundle(),
)
}
override fun logErrorEvent(error: Throwable, params: Map<String, String>) {
params.forEach { fbCrashlytics.setCustomKey(it.key, it.value) }
eventConverter.convertEventParams(params)
.forEach { fbCrashlytics.setCustomKey(it.key, it.value) }
fbCrashlytics.recordException(error)
}

View file

@ -1,12 +1,52 @@
package com.tangem.tap.common.clipboard
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.os.Build
import android.os.PersistableBundle
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
import android.content.ClipboardManager as AndroidClipboardManager
internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager {
override fun setText(label: String, text: String) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text))
override fun setText(text: String, isSensitive: Boolean, label: String) {
val clip = ClipData.newPlainText(label, text).apply {
if (isSensitive) description.setAsSensitive()
}
clipboardManager.setPrimaryClip(clip)
}
override fun getText(default: String?): String? {
val clip = clipboardManager.primaryClip
if (clip == null || clip.itemCount == 0) {
Timber.d("Clipboard is empty")
return default
}
val clipDescription = clipboardManager.primaryClipDescription
if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) {
Timber.d("Clipboard doesn't contain text")
return default
}
return clip.getItemAt(0).text?.toString()
}
private fun ClipDescription.setAsSensitive() {
extras = PersistableBundle().apply {
putBoolean(getExtraIsSensitiveFlag(), true)
}
}
private fun getExtraIsSensitiveFlag(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
}
}
}

View file

@ -4,8 +4,10 @@ import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
internal object MockClipboardManager : ClipboardManager {
override fun setText(label: String, text: String) {
/** Intentionnaly do nothing */
override fun setText(text: String, isSensitive: Boolean, label: String) {
Timber.w("Clipboard Manager not available")
}
override fun getText(default: String?): String? = null
}

View file

@ -1,64 +0,0 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import java.io.File
/**
* required since targetAndroid=30
* <queries>
* <intent>
* <action android:name="android.intent.action.SENDTO" />
* <data android:scheme="mailto" />
* </intent>
* </queries>
*/
@Deprecated("Use EmailSender instead")
fun Activity.sendEmail(
email: String,
subject: String,
message: String,
file: File? = null,
onFail: ((Exception) -> Unit)? = null,
) {
fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
val builder = ShareCompat.IntentBuilder.from(this)
.setType("message/rfc822")
.setEmailTo(arrayOf(recipient))
.setSubject(subject)
.setText(text)
file?.let { builder.setStream(FileProvider.getUriForFile(this, "$packageName.provider", it)) }
return builder.intent
}
val originalIntent = createEmailShareIntent(email, subject, message, file)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, file).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(this, chooserIntent, null)
} catch (ex: Exception) {
onFail?.invoke(ex)
}
}

View file

@ -1,3 +0,0 @@
package com.tangem.tap.common.extensions
fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count))

View file

@ -2,26 +2,19 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.*
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableResId)
}
@ColorInt
fun Fragment.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(requireContext(), colorRes)
}
@ColorInt
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
@ -40,10 +33,6 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
fun View.getQuantityString(@PluralsRes id: Int, quantity: Int): String {
return context.resources.getQuantityString(id, quantity, quantity)
}
fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
return if (show) this.show(invokeBeforeStateChanged) else this.hide(invokeBeforeStateChanged)
}
@ -62,25 +51,6 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
this.visibility = View.GONE
}
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun Context.copyToClipboard(value: Any, label: String = "") {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
val clip: ClipData = ClipData.newPlainText(label, value.toString())
clipboard.setPrimaryClip(clip)
}
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
return clipData.getItemAt(0).text
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, *formatArgs)
}

View file

@ -1,8 +1,5 @@
package com.tangem.tap.common.redux
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.NetworkServices
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
@ -28,7 +25,6 @@ import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import org.rekotlin.Middleware
import org.rekotlin.StateType
@ -46,18 +42,6 @@ data class AppState(
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
private val domainState: DomainState
get() = domainStore.state
private val domainNetworks: NetworkServices
get() = domainState.globalState.networkServices
val featureRepositoryProvider: FeatureRepositoryProvider
get() = FeatureRepositoryProvider(
tangemTechApi = domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
)
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(

View file

@ -1,11 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.home.data.HomeRepositoryImpl
import com.tangem.tap.features.home.domain.HomeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
class FeatureRepositoryProvider(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider) {
val homeRepository: HomeRepository = HomeRepositoryImpl(tangemTechApi, dispatchers)
}

View file

@ -82,8 +82,4 @@ sealed class GlobalAction : Action {
object Update : GlobalAction()
}
object FetchUserCountry : GlobalAction() {
data class Success(val countryCode: String) : GlobalAction()
}
}

View file

@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.util.Locale
object GlobalMiddleware {
val handler = globalMiddlewareHandler
@ -87,27 +86,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
scope.launch { exchangeManager.update() }
}
is GlobalAction.FetchUserCountry -> {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
scope.launch {
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
.onSuccess {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
)
}
.onFailure {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
}
}
}
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.proxy.AppStateHolder
@ -38,7 +36,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
}
is GlobalAction.SaveScanResponse -> {
appStateHolder.scanResponse = action.scanResponse
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
is GlobalAction.ChangeAppCurrency -> {
@ -77,9 +74,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> {
globalState.copy(userCountryCode = action.countryCode)
}
else -> globalState
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
@ -10,6 +11,7 @@ import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
@ -51,7 +53,12 @@ internal class AddressInfoBottomSheetDialog(
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Receive.ButtonCopyAddress())
context.copyToClipboard(data.address)
Toast
.makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT)
.show()
store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Receive.ButtonShareAddress())