Updated on 2026-08-14

This commit is contained in:
Tangem 2020-08-27 12:14:38 +03:00
parent e83c9bc495
commit 2b768f1b00
784 changed files with 2066 additions and 106417 deletions

View file

@ -0,0 +1,70 @@
package com.tangem.tap
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardFilter
import com.tangem.Config
import com.tangem.TangemSdk
import com.tangem.common.extensions.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
import java.util.*
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
get() = Job() + Dispatchers.IO + initCoroutineExceptionHandler()
val scope = CoroutineScope(coroutineContext)
private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, throwable -> throw throwable }
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(
this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
tangemSdkManager = TangemSdkManager(this)
}
override fun onResume() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
if (supportFragmentManager.backStackEntryCount == 0) {
store.dispatch(
NavigationAction.NavigateTo(AppScreen.Home)
)
}
}
override fun onStop() {
notificationsHandler = null
super.onStop()
}
override fun onDestroy() {
store.dispatch(NavigationAction.ActivityDestroyed)
super.onDestroy()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap
import android.app.Application
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import org.rekotlin.Store
val store = Store(
reducer = ::appReducer,
middleware = AppState.getMiddleware(),
state = AppState()
)
class TapApplication : Application()

View file

@ -0,0 +1,3 @@
package com.tangem.tap.common.entities
abstract class Button(val enabled: Boolean)

View file

@ -0,0 +1,15 @@
package com.tangem.tap.common.extensions
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.ByteArrayOutputStream
fun Bitmap.toByteArray(): ByteArray {
val stream = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.JPEG, 20, stream)
return stream.toByteArray()
}
fun ByteArray.toBitmap(): Bitmap {
return BitmapFactory.decodeByteArray(this, 0, this.size)
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.common.extensions
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
val transaction = this.supportFragmentManager.beginTransaction()
.replace(
R.id.fragment_container,
fragmentFactory(screen),
screen.name
)
if (addToBackStack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commit();
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag)
}
fun FragmentActivity.getPreviousScreen(): AppScreen? {
val indexOfLastFragment = this.supportFragmentManager.backStackEntryCount - 1
val tag = this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
return tag?.let { AppScreen.valueOf(tag) }
}
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Wallet -> WalletFragment()
}
}

View file

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

View file

@ -0,0 +1,127 @@
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.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(requireContext(), drawableResId)
}
fun View.show(show: Boolean) {
if (show) this.visibility = View.VISIBLE else this.visibility = View.GONE
}
fun View.show() {
this.visibility = View.VISIBLE
}
fun View.hide() {
this.visibility = View.GONE
}
fun View.makeInvisible() {
this.visibility = View.INVISIBLE
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun View.hideKeyboard() {
val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0)
}
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.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 Fragment.shareText(text: String) {
requireContext().shareText(text)
}
fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGroup) {
if (rootView == null) {
val inflatedView = LayoutInflater.from(context).inflate(viewToInflate, rootView)
parent.addView(inflatedView)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.navigation.navigationReducer
import com.tangem.tap.features.wallet.redux.walletReducer
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?): AppState {
requireNotNull(state)
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = navigationReducer(action, state),
// homeState = homeReducer(action, state),
walletState = walletReducer(action, state),
)
}
sealed class AppAction : Action {
data class RestoreState(val state: AppState) : AppAction()
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.home.redux.homeMiddleware
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.walletMiddleware
import org.rekotlin.Middleware
import org.rekotlin.StateType
data class AppState(
val navigationState: NavigationState = NavigationState(),
val walletState: WalletState = WalletState()
) : StateType {
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
navigationMiddleware, notificationsMiddleware,
homeMiddleware, walletMiddleware,
)
}
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.tap.common.redux
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemError
import com.tangem.tap.notificationsHandler
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
private val coordinatorLayoutWeak = WeakReference(coordinatorLayout)
fun showNotification(message: String) {
coordinatorLayoutWeak.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar -> snackbar.show() }
}
}
fun showNotification(message: Int) {
coordinatorLayoutWeak.get()?.let {
showNotification(it.context.getString(message))
}
}
}
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is NotificationAction) {
notificationsHandler?.showNotification(action.messageResource)
}
if (action is ErrorAction) {
notificationsHandler?.showNotification(action.error.customMessage)
}
next(action)
}
}
}
interface NotificationAction : Action {
val messageResource: Int
}
interface ErrorAction : Action {
val error: TangemError
}

View file

@ -0,0 +1,7 @@
package com.tangem.tap.common.redux
import org.rekotlin.Action
abstract class Request : Action {
abstract suspend fun execute()
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.common.redux.navigation
import androidx.fragment.app.FragmentActivity
import org.rekotlin.Action
import java.lang.ref.WeakReference
sealed class NavigationAction : Action {
data class NavigateTo(val screen: AppScreen, val addToBackstack: Boolean = true) :
NavigationAction()
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.common.redux.navigation
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
val navState = store.state.navigationState
when (action) {
is NavigationAction.NavigateTo -> {
navState.activity?.get()?.openFragment(action.screen, action.addToBackstack)
}
is NavigationAction.PopBackTo -> {
if (action.screen == AppScreen.Home) {
navState.activity?.get()?.popBackTo(null, true)
} else {
navState.activity?.get()?.popBackTo(action.screen)
}
}
}
}
next(action)
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.common.redux.navigation
import com.tangem.tap.common.extensions.getPreviousScreen
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
fun navigationReducer(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 -> {
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 -> navState.copy(activity = null)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.common.redux.navigation
import androidx.fragment.app.FragmentActivity
import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet }

View file

@ -0,0 +1,7 @@
package com.tangem.tap.domain
class BlockchainManager {
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.CommandResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(val activity: ComponentActivity) {
private val tangemSdk = TangemSdk.init(
activity, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
suspend fun scanNote(): CompletionResult<ScanNoteResponse> {
return runTaskAsyncReturnOnMain(ScanNoteTask())
}
private suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> =
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.CommandResponse
import com.tangem.common.CompletionResult
data class ScanNoteResponse(val walletManager: WalletManager) : CommandResponse
class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
val walletManager = session.environment.card?.let { WalletManagerFactory.makeWalletManager(it) }
if (walletManager == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
callback(CompletionResult.Success(ScanNoteResponse(walletManager)))
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.home
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_home.*
class HomeFragment : Fragment(R.layout.fragment_home) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_yes?.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_shop?.setOnClickListener { store.dispatch(HomeAction.GoToShop(requireContext())) }
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.home.redux
import android.content.Context
import org.rekotlin.Action
sealed class HomeAction : Action {
object ReadCard : HomeAction()
data class GoToShop(val context: Context) : HomeAction()
}

View file

@ -0,0 +1,43 @@
package com.tangem.tap.features.home.redux
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat.startActivity
import com.tangem.common.CompletionResult
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
val homeMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is HomeAction.ReadCard -> {
scope.launch {
val result = tangemSdkManager.scanNote()
when (result) {
is CompletionResult.Success -> {
store.dispatch(WalletAction.LoadWallet(result.data.walletManager))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
}
is HomeAction.GoToShop -> {
val uri = Uri.parse(CARD_SHOP_URI)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(action.context, intent, null)
}
}
next(action)
}
}
}
private const val CARD_SHOP_URI = "https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate"

View file

@ -0,0 +1,34 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class WalletAction : Action {
data class LoadWallet(val walletManager: WalletManager) : WalletAction() {
data class Success(val wallet: Wallet): WalletAction()
object Failure: WalletAction()
}
data class LoadPayId(val address: String) : WalletAction() {
object Success: WalletAction()
object Failure: WalletAction()
}
object Scan : WalletAction()
object Send : WalletAction()
object CreatePayId : WalletAction() {
object Success: WalletAction()
object Failure: WalletAction()
}
data class CopyAddress(val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.notification_address_copied
}
}
object ShowQrCode : WalletAction()
object HideQrCode : WalletAction()
data class ExploreAddress(val context: Context) : WalletAction()
object CreateWallet : WalletAction()
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.wallet.redux
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletAction.LoadWallet -> {
scope.launch {
try {
action.walletManager.update()
} catch (ex: Exception) {
store.dispatch(WalletAction.LoadWallet.Failure)
// callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(ex)))
return@launch
}
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadWallet.Success(action.walletManager.wallet))
}
}
}
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote()
when (result) {
is CompletionResult.Success -> {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadWallet(result.data.walletManager))
}
}
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.wallet?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.wallet?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
}
next(action)
}
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import org.rekotlin.Action
fun walletReducer(action: Action, state: AppState): WalletState {
if (action !is WalletAction) return state.walletState
var newState = state.walletState
when (action) {
is WalletAction.LoadWallet -> newState = WalletState(
state = ProgressState.Loading, walletManager = action.walletManager,
currencyData = BalanceWidgetData(
BalanceStatus.Loading, action.walletManager.wallet.blockchain.fullName
)
)
is WalletAction.LoadWallet.Success -> {
val token = action.wallet.amounts[AmountType.Token]
val tokenData = if (token != null) {
TokenData(token.value.toString(), token.currencySymbol)
} else {
null
}
newState = newState.copy(
state = ProgressState.Done, wallet = action.wallet,
currencyData = BalanceWidgetData(
BalanceStatus.VerifiedOnline, action.wallet.blockchain.fullName,
action.wallet.amounts[AmountType.Coin]?.value?.toString(),
token = tokenData
)
)
}
is WalletAction.LoadWallet.Failure -> newState = newState.copy(
state = ProgressState.Done,
currencyData = newState.currencyData.copy(status = BalanceStatus.Unreachable)
)
is WalletAction.ShowQrCode -> {
newState = newState.copy(qrCode = newState.wallet?.shareUrl?.toQrCode())
}
is WalletAction.HideQrCode -> {
newState = newState.copy(qrCode = null)
}
}
return newState
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.PayIdData
import org.rekotlin.StateType
data class WalletState(
val state: ProgressState = ProgressState.Done,
val cardImage: Bitmap? = null,
val walletManager: WalletManager? = null,
val wallet: Wallet? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val qrCode: Bitmap? = null
) : StateType
enum class ProgressState { Loading, Done, Error }

View file

@ -0,0 +1,99 @@
package com.tangem.tap.features.wallet.ui
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_balance.*
import kotlinx.android.synthetic.main.layout_token.view.*
enum class PayIdState {
Loading,
NotCreated,
Loaded,
Error
}
data class PayIdData(
val address: String? = null,
val state: PayIdState? = null
)
enum class BalanceStatus {
VerifiedOnline,
Unreachable,
Loading
}
data class BalanceWidgetData(
val status: BalanceStatus? = null,
val currency: String? = null,
val amount: String? = null,
val fiatAmount: String? = null,
val token: TokenData? = null
)
data class TokenData(
val amount: String,
val tokenSymbol: String
)
class BalanceWidget(
val fragment: Fragment,
val data: BalanceWidgetData,
) {
fun setup() {
when (data.status) {
BalanceStatus.Loading -> {
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = "-"
fragment.tv_fiat_amount.hide()
showStatus(R.id.tv_status_loading)
fragment.l_token.hide()
}
BalanceStatus.VerifiedOnline -> {
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = data.amount
fragment.tv_fiat_amount.text = data.fiatAmount
showStatus(R.id.tv_status_verified)
if (data.token != null) {
fragment.l_token.show()
fragment.l_token.tv_token_symbol.text = data.token.tokenSymbol
fragment.l_token.tv_token_amount.text = data.token.amount
} else {
fragment.l_token.hide()
}
}
BalanceStatus.Unreachable -> {
fragment.l_token.hide()
showStatus(R.id.tv_status_error)
}
}
}
private fun showStatus(@IdRes viewRes: Int) {
fragment.tv_status_error.show(viewRes == R.id.tv_status_error)
fragment.tv_status_loading.show(viewRes == R.id.tv_status_loading)
fragment.tv_status_verified.show(viewRes == R.id.tv_status_verified)
}
}
enum class BalanceErrorWidgetState {
NoAccount,
NoWallet,
BlockchainError
}
class BalanceErrorWidget(
)

View file

@ -0,0 +1,26 @@
package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_qrcode.*
class QrDialog(context: Context) : Dialog(context) {
init {
this.setContentView(R.layout.dialog_qrcode)
}
fun show(qrCode: Bitmap, shareUrl: String) {
this.setOnDismissListener { store.dispatch(WalletAction.HideQrCode) }
this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideQrCode) }
this.tv_qr_dialog_address?.text = shareUrl
this.iv_qrcode?.setImageBitmap(qrCode)
super.show()
}
}

View file

@ -0,0 +1,113 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_address.*
import org.rekotlin.StoreSubscriber
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
var dialog: QrDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletState == newState.walletState
}.select { it.walletState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
btn_scan.setOnClickListener {
store.dispatch(WalletAction.Scan)
}
}
override fun newState(state: WalletState) {
if (activity == null) return
state.wallet?.address?.let { tv_address.text = it }
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
if (state.qrCode != null && state.wallet?.shareUrl != null) {
dialog = QrDialog(requireContext())
dialog?.show(state.qrCode, state.wallet.shareUrl)
} else {
dialog?.dismiss()
}
when (state.state) {
ProgressState.Loading -> {
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
}
ProgressState.Done -> {
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
}
ProgressState.Error -> {
}
}
when (state.payIdData.state) {
PayIdState.Loading -> {
group_payid.hide()
}
PayIdState.NotCreated -> {
group_payid.show()
tv_create_payid.show()
tv_payid_address.hide()
}
PayIdState.Loaded -> {
group_payid.show()
tv_create_payid.hide()
tv_payid_address.show()
tv_payid_address.text = state.payIdData.address
}
PayIdState.Error -> group_payid.hide()
}
}
}