Updated on 2026-08-14
This commit is contained in:
commit
bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.common
|
|||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.ui.*
|
||||
|
|
@ -46,7 +46,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
|
||||
dialog = when (state.dialog) {
|
||||
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
|
||||
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source)
|
||||
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(
|
||||
context = context,
|
||||
source = state.dialog.source,
|
||||
onTryAgain = state.dialog.onTryAgain,
|
||||
)
|
||||
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
|
||||
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
|
||||
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSources) : AnalyticsEvent(
|
||||
category = "Cant Scan The Card",
|
||||
event = button.event,
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
) {
|
||||
enum class Buttons(val event: String) {
|
||||
TRY_AGAIN("Try again button"),
|
||||
HOW_TO_SCAN("Button blog"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +1,51 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.*
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.FragmentShareTransition
|
||||
import com.tangem.feature.referral.ReferralFragment
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
||||
import com.tangem.tap.features.details.ui.details.DetailsFragment
|
||||
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
|
||||
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
|
||||
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
|
||||
import com.tangem.tap.features.home.HomeFragment
|
||||
import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment
|
||||
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
|
||||
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
|
||||
import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
|
||||
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
|
||||
import com.tangem.tap.features.send.ui.SendFragment
|
||||
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
|
||||
import com.tangem.tap.features.welcome.ui.WelcomeFragment
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
|
||||
fun FragmentActivity.openFragment(
|
||||
screen: AppScreen,
|
||||
addToBackstack: Boolean,
|
||||
bundle: Bundle? = null,
|
||||
fgShareTransition: FragmentShareTransition? = null,
|
||||
) {
|
||||
val transaction = supportFragmentManager.beginTransaction().apply {
|
||||
setReorderingAllowed(true)
|
||||
}
|
||||
fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider<Fragment>) {
|
||||
Timber.d("Showing $name route")
|
||||
|
||||
val fragment = fragmentFactory(screen).apply {
|
||||
arguments = bundle
|
||||
val isPoppedBack = popBackStackImmediate(name, 0)
|
||||
|
||||
if (fgShareTransition != null) {
|
||||
sharedElementEnterTransition = fgShareTransition.enterTransitionSet
|
||||
sharedElementReturnTransition = fgShareTransition.exitTransitionSet
|
||||
fgShareTransition.shareElements.forEach { shareElement ->
|
||||
shareElement.wView.get()?.let { view ->
|
||||
transaction.addSharedElement(view, shareElement.elementName)
|
||||
}
|
||||
}
|
||||
if (!isPoppedBack) {
|
||||
val fragment = fragmentProvider()
|
||||
|
||||
if (fragment is DialogFragment) {
|
||||
fragment.showDialog(fragmentManager = this, name)
|
||||
} else {
|
||||
fragment.showFragment(fragmentManager = this, name)
|
||||
}
|
||||
}
|
||||
|
||||
if (screen.isDialogFragment && fragment is DialogFragment) {
|
||||
fragment.showAllowingStateLoss(
|
||||
fragmentManager = supportFragmentManager,
|
||||
baseTransaction = transaction,
|
||||
tag = screen.name,
|
||||
addToBackstack = addToBackstack,
|
||||
)
|
||||
Timber.d("Route $name is shown")
|
||||
} else {
|
||||
transaction.replace(R.id.fragment_container, fragment, screen.name)
|
||||
if (addToBackstack) {
|
||||
transaction.addToBackStack(screen.name)
|
||||
}
|
||||
Timber.d("Route $name is found in backstack and shown")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
try {
|
||||
transaction.addToBackStack(name)
|
||||
show(transaction, name)
|
||||
} catch (e: IllegalStateException) {
|
||||
transaction.add(this, name)
|
||||
transaction.addToBackStack(name)
|
||||
|
||||
transaction.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun DialogFragment.showAllowingStateLoss(
|
||||
fragmentManager: FragmentManager,
|
||||
baseTransaction: FragmentTransaction,
|
||||
tag: String,
|
||||
addToBackstack: Boolean,
|
||||
) {
|
||||
runCatching {
|
||||
if (addToBackstack) baseTransaction.addToBackStack(tag)
|
||||
show(baseTransaction, tag)
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
if (throwable is IllegalStateException) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
transaction.add(this, tag)
|
||||
if (addToBackstack) transaction.addToBackStack(tag)
|
||||
private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
transaction.commitAllowingStateLoss()
|
||||
} else {
|
||||
Timber.e(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction.replace(R.id.fragment_container, this, name)
|
||||
transaction.addToBackStack(name)
|
||||
|
||||
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
|
||||
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
|
||||
try {
|
||||
this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag)
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.getPreviousScreen(): AppScreen? {
|
||||
val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) {
|
||||
this.supportFragmentManager.backStackEntryCount - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount) {
|
||||
this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return tag?.let { AppScreen.valueOf(tag) }
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
private fun fragmentFactory(screen: AppScreen): Fragment {
|
||||
return when (screen) {
|
||||
AppScreen.Home -> HomeFragment()
|
||||
AppScreen.OnboardingNote -> OnboardingNoteFragment()
|
||||
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
|
||||
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
|
||||
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
|
||||
AppScreen.Wallet -> {
|
||||
store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment()
|
||||
}
|
||||
AppScreen.Send -> {
|
||||
val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles)
|
||||
|
||||
if (featureToggles.isRedesignedSendEnabled) {
|
||||
store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment()
|
||||
} else {
|
||||
SendFragment()
|
||||
}
|
||||
}
|
||||
AppScreen.Details -> {
|
||||
val featureToggles = store.inject(getDependency = DaggerGraphState::detailsFeatureToggles)
|
||||
|
||||
if (featureToggles.isRedesignEnabled) {
|
||||
store.inject(DaggerGraphState::detailsEntryPoint).entryFragment()
|
||||
} else {
|
||||
DetailsFragment()
|
||||
}
|
||||
}
|
||||
AppScreen.DetailsSecurity -> SecurityModeFragment()
|
||||
AppScreen.CardSettings -> CardSettingsFragment()
|
||||
AppScreen.AppSettings -> AppSettingsFragment()
|
||||
AppScreen.ResetToFactory -> ResetCardFragment()
|
||||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
AppScreen.ManageTokens -> TokensListFragment()
|
||||
AppScreen.AddCustomToken -> AddCustomTokenFragment()
|
||||
AppScreen.WalletDetails -> {
|
||||
store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment()
|
||||
}
|
||||
AppScreen.WalletConnectSessions -> WalletConnectFragment()
|
||||
AppScreen.QrScanning -> {
|
||||
store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment()
|
||||
}
|
||||
AppScreen.ReferralProgram -> ReferralFragment()
|
||||
AppScreen.Swap -> SwapFragment()
|
||||
AppScreen.Welcome -> WelcomeFragment()
|
||||
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
|
||||
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
|
||||
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
|
||||
}
|
||||
transaction.commitAllowingStateLoss()
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -82,12 +82,16 @@ suspend fun dispatchOnMain(vararg actions: Action) {
|
|||
withMainContext { actions.forEach { store.dispatch(it) } }
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchOpenUrl(url: String) {
|
||||
dispatch(NavigationAction.OpenUrl(url))
|
||||
fun Store<AppState>.dispatchOpenUrl(url: String) {
|
||||
inject(DaggerGraphState::urlOpener).openUrl(url)
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchShare(url: String) {
|
||||
dispatch(NavigationAction.Share(url))
|
||||
fun Store<AppState>.dispatchShare(url: String) {
|
||||
inject(DaggerGraphState::shareManager).shareText(url)
|
||||
}
|
||||
|
||||
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
|
||||
inject(DaggerGraphState::appRouter).action()
|
||||
}
|
||||
|
||||
inline fun <reified T> Store<AppState>.inject(getDependency: DaggerGraphState.() -> T?): T {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
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.*
|
||||
|
|
@ -78,16 +81,6 @@ fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
|
|||
return clipData.getItemAt(0).text
|
||||
}
|
||||
|
||||
fun Context.shareText(text: String) {
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
|
||||
return context.getString(resId, *formatArgs)
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ import java.io.StringWriter
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeedbackManager(
|
||||
class LegacyFeedbackManager(
|
||||
val infoHolder: AdditionalFeedbackInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
private val chatManager: ChatManager,
|
||||
|
|
@ -144,7 +144,7 @@ class FeedbackManager(
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private companion object {
|
||||
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
|
||||
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
|
||||
const val FEEDBACK_FILE = "feedback.txt"
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import com.tangem.core.navigation.feedback.FeedbackManager
|
||||
import com.tangem.core.navigation.feedback.FeedbackType
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ProxyFeedbackManager : FeedbackManager {
|
||||
|
||||
override fun sendEmail(type: FeedbackType) {
|
||||
val manager = store.state.globalState.feedbackManager
|
||||
|
||||
if (manager == null) {
|
||||
Timber.e("Feedback manager is not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
val data = when (type) {
|
||||
is FeedbackType.Feedback -> FeedbackEmail()
|
||||
is FeedbackType.RateCanBeBetter -> RateCanBeBetterEmail()
|
||||
is FeedbackType.ScanFails -> ScanFailsEmail()
|
||||
is FeedbackType.SendTransactionFailed -> SendTransactionFailedEmail(type.error)
|
||||
is FeedbackType.Support -> FeedbackEmail()
|
||||
}
|
||||
|
||||
manager.sendEmail(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.common.finisher
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
|
||||
internal class AndroidAppFinisher(
|
||||
private val appContext: Context,
|
||||
) : AppFinisher {
|
||||
|
||||
override fun finish() {
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun restart() {
|
||||
val intent = Intent(appContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
appContext.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.tap.common.pushes
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.executeBlocking
|
||||
import coil.request.ImageRequest
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
|
||||
internal class TangemPushNotificationService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Timber.d("New FCM token received: $token")
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
super.onMessageReceived(message)
|
||||
|
||||
val notification = message.notification ?: return
|
||||
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
|
||||
|
||||
// TODO refactoring: [REDACTED_JIRA]
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
/* context = */ this,
|
||||
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
|
||||
/* intent = */ intent,
|
||||
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
|
||||
val notificationBuilder =
|
||||
NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setSmallIcon(R.drawable.ic_tangem_24)
|
||||
.setContentTitle(notification.title)
|
||||
.setContentText(notification.body)
|
||||
.setPriority(message.priority)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setVibrate(notification.vibrateTimings)
|
||||
.apply {
|
||||
notification.imageUrl?.let { uri ->
|
||||
val bitmap = getBitmapImageFromUrl(uri)
|
||||
setStyle(
|
||||
NotificationCompat
|
||||
.BigPictureStyle()
|
||||
.bigPicture(bitmap),
|
||||
).setLargeIcon(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannel = NotificationChannel(
|
||||
channelId,
|
||||
ContextCompat.getString(applicationContext, R.string.tangem_app_name),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
|
||||
// Generating unique notification id
|
||||
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
|
||||
|
||||
notificationManager.notify(
|
||||
/* id = */ uniqueId,
|
||||
/* notification = */ notificationBuilder.build(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
|
||||
return createCoilImageLoader(
|
||||
applicationContext,
|
||||
logEnabled = LogConfig.imageLoader,
|
||||
).executeBlocking(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(url)
|
||||
.build(),
|
||||
).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
|
||||
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.TestAction
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.domain.model.WalletAddressData
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.common.redux.global.globalReducer
|
||||
import com.tangem.tap.common.redux.navigation.NavigationReducer
|
||||
import com.tangem.tap.features.details.redux.DetailsReducer
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
|
||||
|
|
@ -23,7 +22,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
|
|||
if (action is AppAction.RestoreState) return action.state
|
||||
|
||||
return AppState(
|
||||
navigationState = NavigationReducer.reduce(action, state),
|
||||
globalState = globalReducer(action, state, appStateHolder),
|
||||
homeState = HomeReducer.reduce(action, state),
|
||||
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.core.navigation.NavigationState
|
||||
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
|
||||
import com.tangem.tap.common.redux.navigation.navigationMiddleware
|
||||
import com.tangem.tap.features.details.redux.DetailsMiddleware
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
|
||||
|
|
@ -40,7 +38,6 @@ import org.rekotlin.Middleware
|
|||
import org.rekotlin.StateType
|
||||
|
||||
data class AppState(
|
||||
val navigationState: NavigationState = NavigationState(),
|
||||
val globalState: GlobalState = GlobalState(),
|
||||
val homeState: HomeState = HomeState(),
|
||||
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
|
||||
|
|
@ -73,7 +70,6 @@ data class AppState(
|
|||
fun getMiddleware(): List<Middleware<AppState>> {
|
||||
return listOf(
|
||||
logMiddleware,
|
||||
navigationMiddleware,
|
||||
notificationsMiddleware,
|
||||
GlobalMiddleware.handler,
|
||||
HomeMiddleware.handler,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.feedback.LegacyFeedbackManager
|
||||
import com.tangem.tap.common.redux.DebugErrorAction
|
||||
import com.tangem.tap.common.redux.ErrorAction
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
|
|
@ -79,7 +79,7 @@ sealed class GlobalAction : Action {
|
|||
|
||||
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
|
||||
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
|
||||
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
|
||||
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()
|
||||
|
||||
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.feedback.LegacyFeedbackManager
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
|
|
@ -20,7 +20,7 @@ data class GlobalState(
|
|||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||
val configManager: ConfigManager? = null,
|
||||
val warningManager: WarningMessagesManager? = null,
|
||||
val feedbackManager: FeedbackManager? = null,
|
||||
val feedbackManager: LegacyFeedbackManager? = null,
|
||||
val appCurrency: AppCurrency = AppCurrency.Default,
|
||||
val scanCardFailsCounter: Int = 0,
|
||||
val dialog: StateDialog? = null,
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
package com.tangem.tap.common.redux.navigation
|
||||
|
||||
import android.content.Intent
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.tap.activityResultCaller
|
||||
import com.tangem.tap.common.CustomTabsManager
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
val navigationMiddleware: Middleware<AppState> = { _, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
if (action is NavigationAction) {
|
||||
val navState = state()?.navigationState
|
||||
when (action) {
|
||||
is NavigationAction.NavigateTo -> {
|
||||
navState?.activity?.get()?.openFragment(
|
||||
screen = action.screen,
|
||||
addToBackstack = action.addToBackstack,
|
||||
fgShareTransition = action.fragmentShareTransition,
|
||||
bundle = action.bundle,
|
||||
)
|
||||
}
|
||||
is NavigationAction.PopBackTo -> {
|
||||
if (navState?.backStack?.lastOrNull() != action.screen) {
|
||||
when (val screen = action.screen) {
|
||||
AppScreen.Home,
|
||||
AppScreen.Welcome,
|
||||
-> {
|
||||
if (navState?.backStack?.contains(screen) == false) {
|
||||
// Pop back to activity
|
||||
navState.activity?.get()?.popBackTo(screen = null, inclusive = true)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
|
||||
} else {
|
||||
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is NavigationAction.OpenUrl -> {
|
||||
navState?.activity?.get()?.let {
|
||||
CustomTabsManager().openUrl(action.url, it)
|
||||
}
|
||||
}
|
||||
is NavigationAction.OpenDocument -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = action.url
|
||||
navState?.activity?.get()?.startActivity(intent)
|
||||
}
|
||||
is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog)
|
||||
is NavigationAction.OpenBiometricsSettings -> {
|
||||
val settingsAction = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
Settings.ACTION_BIOMETRIC_ENROLL
|
||||
}
|
||||
else -> {
|
||||
Settings.ACTION_SECURITY_SETTINGS
|
||||
}
|
||||
}
|
||||
val intent = Intent(settingsAction).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
putExtra(
|
||||
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
activityResultCaller.activityResultLauncher?.launch(intent)
|
||||
}
|
||||
is NavigationAction.Share -> {
|
||||
navState?.activity?.get()?.shareText(action.data)
|
||||
}
|
||||
is NavigationAction.ActivityCreated,
|
||||
is NavigationAction.ActivityDestroyed,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package com.tangem.tap.common.redux.navigation
|
||||
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.NavigationState
|
||||
import com.tangem.tap.common.extensions.getPreviousScreen
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import org.rekotlin.Action
|
||||
|
||||
object NavigationReducer {
|
||||
fun reduce(action: Action, state: AppState): NavigationState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
private fun internalReduce(action: Action, state: AppState): NavigationState {
|
||||
val navigationAction = action as? NavigationAction ?: return state.navigationState
|
||||
val navState = state.navigationState
|
||||
|
||||
return when (navigationAction) {
|
||||
is NavigationAction.NavigateTo -> {
|
||||
navState.copy(backStack = navState.backStack + navigationAction.screen)
|
||||
}
|
||||
is NavigationAction.PopBackTo -> {
|
||||
if (navState.backStack.lastOrNull() == navigationAction.screen) return navState
|
||||
|
||||
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
|
||||
val index = navState.backStack.lastIndexOf(screen) + 1
|
||||
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
|
||||
}
|
||||
is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity)
|
||||
is NavigationAction.ActivityDestroyed -> {
|
||||
when {
|
||||
// Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a
|
||||
// new scan from the background [REDACTED_TASK_KEY]
|
||||
navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null)
|
||||
else -> navState
|
||||
}
|
||||
}
|
||||
else -> navState
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.common.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
|
||||
internal class IntentSettingsManager(val context: Context) : SettingsManager {
|
||||
override fun openSettings() {
|
||||
val openSettingsIntent = Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
)
|
||||
context.startActivity(openSettingsIntent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.tap.common.share
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
|
||||
internal class IntentShareManager : ShareManager {
|
||||
|
||||
override fun shareText(text: String) {
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
|
||||
activity.startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
|
|
@ -44,7 +43,7 @@ class RussianCardholdersWarningBottomSheetDialog(
|
|||
dismiss()
|
||||
}
|
||||
binding?.btnNo?.setOnClickListener {
|
||||
store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL))
|
||||
store.dispatchOpenUrl(INSTRUCTION_URL)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,76 @@
|
|||
package com.tangem.tap.common.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import androidx.core.view.isVisible
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ScanFailsDialog {
|
||||
fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog {
|
||||
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
|
||||
setTitle(context.getString(R.string.common_warning))
|
||||
setMessage(R.string.alert_troubleshooting_scan_card_title)
|
||||
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
|
||||
val sourceAnalytics = when (source) {
|
||||
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
|
||||
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
|
||||
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
|
||||
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
|
||||
internal object ScanFailsDialog {
|
||||
|
||||
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
|
||||
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
|
||||
|
||||
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
|
||||
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
|
||||
val customView = View.inflate(context, R.layout.dialog_scan_fails, null)
|
||||
val sourceAnalytics = when (source) {
|
||||
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
|
||||
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
|
||||
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
|
||||
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
|
||||
}
|
||||
val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button)
|
||||
if (onTryAgain != null) {
|
||||
tryAgainBtn?.isVisible = true
|
||||
tryAgainBtn?.setOnClickListener {
|
||||
store.dispatchDialogHide()
|
||||
Analytics.send(
|
||||
ScanFailsDialogAnalytics(
|
||||
button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN,
|
||||
source = sourceAnalytics,
|
||||
),
|
||||
)
|
||||
onTryAgain()
|
||||
}
|
||||
} else {
|
||||
tryAgainBtn?.isVisible = false
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.how_to_scan_button)?.setOnClickListener {
|
||||
Analytics.send(
|
||||
ScanFailsDialogAnalytics(
|
||||
button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN,
|
||||
source = sourceAnalytics,
|
||||
),
|
||||
)
|
||||
val locale = LocaleRegionProvider().getRegion()
|
||||
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
|
||||
store.dispatchOpenUrl(link)
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
|
||||
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
|
||||
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
|
||||
}
|
||||
setNeutralButton(R.string.common_cancel) { _, _ -> }
|
||||
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
|
||||
store.dispatchDialogHide()
|
||||
}
|
||||
setView(customView)
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
}.create()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.common
|
||||
package com.tangem.tap.common.url
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
|
@ -8,12 +8,22 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
|
|||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
|
||||
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.common.extensions.getColorCompat
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import com.tangem.wallet.R
|
||||
|
||||
class CustomTabsManager {
|
||||
fun openUrl(url: String, context: Context) {
|
||||
internal class CustomTabsUrlOpener : UrlOpener {
|
||||
|
||||
override fun openUrl(url: String) {
|
||||
foregroundActivityObserver.withForegroundActivity {
|
||||
openUrl(url, context = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String, context: Context) {
|
||||
if (url.isEmpty()) return
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
Loading…
Add table
Add a link
Reference in a new issue