Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-01 21:38:18 +03:00
parent f21c4adfd2
commit 14f4b46e19
23 changed files with 463 additions and 87 deletions

View file

@ -64,7 +64,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'com.tangem:blockchain:1.25.0'
implementation 'com.tangem:blockchain:1.27.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"

View file

@ -1,7 +1,10 @@
package com.tangem.tap
import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardFilter
import com.tangem.Config
@ -76,4 +79,15 @@ class MainActivity : AppCompatActivity() {
store.dispatch(NavigationAction.ActivityDestroyed)
super.onDestroy()
}
}
fun setTranslucent(activity: Activity, translucent: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val w = activity.getWindow();
if (translucent) {
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
} else {
w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
}

View file

@ -3,7 +3,10 @@ package com.tangem.tap
import android.app.Application
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
import timber.log.Timber
val store = Store(
reducer = ::appReducer,
@ -11,4 +14,13 @@ val store = Store(
state = AppState()
)
class TapApplication : Application()
class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
NetworkConnectivity(store, this)
}
}

View file

@ -5,6 +5,7 @@ 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.send.ui.SendFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.R
@ -34,5 +35,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
}
}

View file

@ -29,14 +29,17 @@ fun View.show(show: Boolean) {
}
fun View.show() {
if (this.visibility == View.VISIBLE) return
this.visibility = View.VISIBLE
}
fun View.hide() {
if (this.visibility == View.GONE) return
this.visibility = View.GONE
}
fun View.makeInvisible() {
if (this.visibility == View.INVISIBLE) return
this.visibility = View.INVISIBLE
}

View file

@ -2,16 +2,19 @@ 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.send.redux.SendReducer
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),
globalState = globalReducer(action, state),
walletState = walletReducer(action, state),
navigationState = navigationReducer(action, state),
globalState = globalReducer(action, state),
walletState = walletReducer(action, state),
sendState = SendReducer.reduce(action, state.sendState)
)
}

View file

@ -4,21 +4,24 @@ import com.tangem.tap.common.redux.global.GlobalState
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.send.redux.SendState
import com.tangem.tap.features.send.redux.logMiddleware
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 globalState: GlobalState = GlobalState(),
val walletState: WalletState = WalletState()
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
) : StateType {
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
navigationMiddleware, notificationsMiddleware,
logMiddleware, navigationMiddleware, notificationsMiddleware,
homeMiddleware, walletMiddleware,
)
}

View file

@ -9,4 +9,4 @@ data class NavigationState(
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet }
enum class AppScreen { Home, Wallet, Send }

View file

@ -0,0 +1,45 @@
package com.tangem.tap.features.send
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
import kotlinx.android.synthetic.main.fragment_wallet.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
abstract fun subscribeToStore()
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
}
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
super.onStop()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.send.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
val logMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
Timber.d("$action")
nextDispatch(action)
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.send.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
interface SendAction : Action
sealed class FeeLayout : SendAction {
object ToggleFeeLayoutVisibility : FeeLayout()
data class ChangeSelectedFee(val id: Int) : FeeLayout()
class ChangeLoremState(val isChecked: Boolean) : FeeLayout()
}

View file

@ -0,0 +1,45 @@
package com.tangem.tap.features.send.redux
import android.view.View
import org.rekotlin.Action
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
class SendReducer {
companion object {
fun reduce(action: Action, state: SendState): SendState {
val sendAction = action as? SendAction ?: return state
return when (sendAction) {
is FeeLayout -> handleFeeLayoutAction(sendAction, state, state.feeLayoutState)
else -> state
}
}
private fun handleFeeLayoutAction(action: FeeLayout, sendState: SendState, state: FeeLayoutState): SendState {
return when (action) {
is FeeLayout.ToggleFeeLayoutVisibility -> {
val visibility = if (state.visibility == View.VISIBLE) View.GONE
else View.VISIBLE
val result = state.copy(visibility = visibility)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
is FeeLayout.ChangeSelectedFee -> {
val result = state.copy(selectedFeeId = action.id)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
is FeeLayout.ChangeLoremState -> {
val result = state.copy(swLoremIsChecked = action.isChecked)
updateLastState(sendState.copy(feeLayoutState = result), result)
}
}
}
private fun updateLastState(sendState: SendState, state: StateType): SendState {
return sendState.copy(lastChangedStateType = state)
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.send.redux
import android.view.View
import com.tangem.wallet.R
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class SendState(
val lastChangedStateType: StateType = NoneState(),
val feeLayoutState: FeeLayoutState = FeeLayoutState()
) : StateType
class NoneState : StateType
data class FeeLayoutState(
val visibility: Int = View.GONE,
val selectedFeeId: Int = R.id.chipNormal,
val swLoremIsChecked: Boolean = false
) : StateType

View file

@ -0,0 +1,49 @@
package com.tangem.tap.features.send.ui
import android.os.Bundle
import android.view.View
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.send.redux.FeeLayout
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.send.ui.stateSubscribers.WalletStateSubscriber
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_send_network_fee.*
/**
[REDACTED_AUTHOR]
*/
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private val walletSubscriber = WalletStateSubscriber(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
flExpandCollapse.setOnClickListener {
store.dispatch(FeeLayout.ToggleFeeLayoutVisibility)
}
chipGroup.setOnCheckedChangeListener { group, checkedId ->
store.dispatch(FeeLayout.ChangeSelectedFee(checkedId))
}
swLorem.setOnCheckedChangeListener { btn, isChecked ->
store.dispatch(FeeLayout.ChangeLoremState(isChecked))
}
}
override fun subscribeToStore() {
store.subscribe(walletSubscriber) { appState ->
appState.skipRepeats { oldState, newState -> false }.select { it.walletState }
}
store.subscribe(sendSubscriber) { appState ->
appState.skipRepeats { oldState, newState -> false }.select { it.sendState }
}
storeSubscribersList.add(walletSubscriber)
storeSubscribersList.add(sendSubscriber)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import androidx.fragment.app.Fragment
import org.rekotlin.StateType
import org.rekotlin.StoreSubscriber
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
abstract class FragmentStateSubscriber<S : StateType>(fragment: Fragment) : StoreSubscriber<S> {
private val weakFragment: WeakReference<Fragment> = WeakReference(fragment)
abstract fun updateWithNewState(fg: Fragment, state: S)
override fun newState(state: S) {
val fg = weakFragment.get() ?: return
updateWithNewState(fg, state)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.transition.TransitionManager
import com.tangem.tap.features.send.redux.FeeLayoutState
import com.tangem.tap.features.send.redux.SendState
import kotlinx.android.synthetic.main.btn_expand_collapse.*
import kotlinx.android.synthetic.main.layout_send_network_fee.*
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendState>(fragment) {
override fun updateWithNewState(fg: Fragment, state: SendState) {
when (state.lastChangedStateType) {
is FeeLayoutState -> handleFeeLayoutState(fg, state.feeLayoutState)
}
}
private fun handleFeeLayoutState(fg: Fragment, layoutState: FeeLayoutState) {
Timber.d("handleFeeLayoutState")
if (fg.llFeeContainer.visibility != layoutState.visibility) {
val rotationAngle = if (fg.imvExpandCollapse.rotation == 0f) 180f else 0f
fg.imvExpandCollapse.rotation = rotationAngle
(fg.llFeeContainer.parent?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) }
fg.llFeeContainer.visibility = layoutState.visibility
}
if (fg.swLorem.isChecked != layoutState.swLoremIsChecked) {
fg.swLorem.isChecked = layoutState.swLoremIsChecked
}
if (fg.chipGroup.checkedChipId != layoutState.selectedFeeId) {
fg.chipGroup.check(layoutState.selectedFeeId)
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import androidx.fragment.app.Fragment
import com.tangem.tap.features.wallet.redux.WalletState
/**
[REDACTED_AUTHOR]
*/
class WalletStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<WalletState>(fragment) {
override fun updateWithNewState(fg: Fragment, state: WalletState) {
}
}

View file

@ -6,6 +6,7 @@ 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.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.PayIdState
@ -55,6 +56,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
btn_scan.setOnClickListener {
store.dispatch(WalletAction.Scan)
}
btn_send.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
override fun newState(state: WalletState) {

View file

@ -0,0 +1,40 @@
package com.tangem.tap.network
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import org.rekotlin.Action
import org.rekotlin.Store
/**
[REDACTED_AUTHOR]
*/
class NetworkConnectivity(
private val store: Store<*>,
context: Context
) {
private val connectivityManager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
}
}
init {
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(receiver, intentFilter)
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
}
fun isOnlineOrConnecting(): Boolean {
val netInfo = connectivityManager.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
}
}
data class NetworkStateChanged(val isOnline: Boolean) : Action

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 24% opacity -->
<item android:alpha="0.24" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
<item android:alpha="0.24" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
<!-- 12% of 87% opacity -->
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
<item android:color="@android:color/transparent" android:state_enabled="true" />
<item android:color="@android:color/transparent" />

View file

@ -3,49 +3,70 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:orientation="vertical">
<include
layout="@layout/layout_send_address_payid"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp" />
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/send_title" />
<include
layout="@layout/layout_send_currency"
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
android:layout_height="wrap_content">
<include
android:id="@+id/clNetworkFee"
layout="@layout/layout_send_network_fee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<include
layout="@layout/layout_send_address_payid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp" />
<include
android:id="@+id/clNetworkFee"
layout="@layout/layout_send_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp" />
<include
layout="@layout/layout_send_currency"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<com.google.android.material.button.MaterialButton
style="@style/TapButtonWithIcon"
android:layout_width="200dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_btn_send"
app:icon="@drawable/ic_arrow_right" />
<include
android:id="@+id/clNetworkFee"
layout="@layout/layout_send_network_fee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<include
android:id="@+id/clNetworkFee"
layout="@layout/layout_send_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp" />
<com.google.android.material.button.MaterialButton
style="@style/TapButtonWithIcon"
android:layout_width="200dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_btn_send"
app:icon="@drawable/ic_arrow_right" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -13,12 +13,12 @@
android:layout_marginStart="16dp"
android:text="@string/send_network_fee"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@+id/include"
app:layout_constraintBottom_toBottomOf="@+id/flExpandCollapse"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/include" />
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/include"
android:id="@+id/flExpandCollapse"
layout="@layout/btn_expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -26,51 +26,58 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
<LinearLayout
android:id="@+id/llFeeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:translationY="-8dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include"
app:singleLine="true"
app:singleSelection="true">
app:layout_constraintTop_toBottomOf="@+id/flExpandCollapse">
<com.google.android.material.chip.Chip
android:id="@+id/chipLow"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_low" />
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:translationY="-8dp"
app:singleLine="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipNormal"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
<com.google.android.material.chip.Chip
android:id="@+id/chipLow"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_low" />
<com.google.android.material.chip.Chip
android:id="@+id/chipNormal"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_normal" />
<com.google.android.material.chip.Chip
android:id="@+id/chipPriority"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_priority" />
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/swLorem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_normal" />
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. At tristique ornare auctor in." />
<com.google.android.material.chip.Chip
android:id="@+id/chipPriority"
style="@style/ChipNetworkFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_network_chip_fee_priority" />
</LinearLayout>
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. At tristique ornare auctor in."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroup" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -31,6 +31,7 @@
<string name="error_creating_payid">Error response while creating PayID.</string>
<string name="send_title">Send</string>
<string name="send_address_or_payid">Address or PayID</string>
<string name="send_network_fee">Network fee</string>
<string name="send_network_chip_fee_low">Low</string>