Updated on 2026-08-14
This commit is contained in:
parent
e83c9bc495
commit
2b768f1b00
784 changed files with 2066 additions and 106417 deletions
70
app/src/main/java/com/tangem/tap/MainActivity.kt
Normal file
70
app/src/main/java/com/tangem/tap/MainActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/com/tangem/tap/TapApplication.kt
Normal file
14
app/src/main/java/com/tangem/tap/TapApplication.kt
Normal 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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.tap.common.entities
|
||||
|
||||
abstract class Button(val enabled: Boolean)
|
||||
15
app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt
Normal file
15
app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
127
app/src/main/java/com/tangem/tap/common/extensions/UI.kt
Normal file
127
app/src/main/java/com/tangem/tap/common/extensions/UI.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
Normal file
19
app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
Normal 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()
|
||||
}
|
||||
25
app/src/main/java/com/tangem/tap/common/redux/AppState.kt
Normal file
25
app/src/main/java/com/tangem/tap/common/redux/AppState.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
7
app/src/main/java/com/tangem/tap/common/redux/Request.kt
Normal file
7
app/src/main/java/com/tangem/tap/common/redux/Request.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import org.rekotlin.Action
|
||||
|
||||
abstract class Request : Action {
|
||||
abstract suspend fun execute()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
class BlockchainManager {
|
||||
|
||||
|
||||
|
||||
}
|
||||
41
app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
Normal file
41
app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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())) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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(
|
||||
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue