checkpoint: send is functional and shows up in active transactions.

This commit is contained in:
Kevin Gorham 2019-02-07 22:18:32 -05:00 committed by Kevin Gorham
parent 3987fe820b
commit 78c18faee0
20 changed files with 250 additions and 235 deletions

View File

@ -49,15 +49,17 @@ dependencies {
implementation deps.androidx.navigation.ui implementation deps.androidx.navigation.ui
implementation deps.androidx.navigation.uiKtx implementation deps.androidx.navigation.uiKtx
implementation deps.material implementation deps.material
implementation 'androidx.multidex:multidex:2.0.1' implementation deps.androidx.multidex
// Kotlin // Kotlin
implementation deps.kotlin.stdlib implementation deps.kotlin.stdlib
implementation deps.kotlin.reflect
implementation deps.kotlin.coroutines.core implementation deps.kotlin.coroutines.core
implementation deps.kotlin.coroutines.android implementation deps.kotlin.coroutines.android
// Zcash // Zcash
implementation deps.zcash.walletSdk implementation deps.zcash.walletSdk
compile project(path: ':qrecycler')
// TODO: get the AAR to provide these // TODO: get the AAR to provide these
implementation "io.grpc:grpc-okhttp:1.17.1" implementation "io.grpc:grpc-okhttp:1.17.1"
@ -75,11 +77,10 @@ dependencies {
// Other // Other
implementation deps.speeddial implementation deps.speeddial
implementation "com.airbnb.android:lottie:2.7.0" implementation deps.lottie
compile 'com.facebook.stetho:stetho:1.5.0' debugImplementation deps.stetho
testImplementation deps.junit testImplementation deps.junit
androidTestImplementation deps.androidx.test.runner androidTestImplementation deps.androidx.test.runner
androidTestImplementation deps.androidx.test.espresso androidTestImplementation deps.androidx.test.espresso
compile project(path: ':qrecycler')
} }

View File

@ -1,89 +0,0 @@
package cash.z.android.wallet.data
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.vo.WalletTransaction
import cash.z.android.wallet.vo.WalletTransactionStatus
import cash.z.wallet.sdk.dao.BlockDao
import cash.z.wallet.sdk.dao.NoteDao
import cash.z.wallet.sdk.dao.TransactionDao
import cash.z.wallet.sdk.db.DerivedDataDb
import cash.z.wallet.sdk.vo.NoteQuery
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import java.math.BigDecimal
class ReceivedTransactionRepository(val scope: CoroutineScope) : TransactionRepository {
private var db = injectDb()
private lateinit var transactions: TransactionDao
private lateinit var blocks: BlockDao
private lateinit var notes: NoteDao
private var existingTransactions = linkedSetOf<WalletTransaction>()
private var existingBalance = BigDecimal.ZERO
private var balanceChannel = BroadcastChannel<BigDecimal>(100)
private fun injectDb() = Room
.databaseBuilder(ZcashWalletApplication.instance, DerivedDataDb::class.java, "tmp")
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.fallbackToDestructiveMigration()
.build()
.apply {
transactions = transactionDao()
blocks = blockDao()
notes = noteDao()
}
/**
* Just send a sample stream of balances, every so often
*/
override fun balance() = balanceChannel.openSubscription()
/**
* Just send a sample stream of transactions, every so often
*/
override fun transactions(): ReceiveChannel<WalletTransaction> = scope.produce {
while (isActive) {
delay(1500L)
val newTransactions = checkForNewTransactions()
newTransactions?.forEach {
existingTransactions.add(it)
send(it)
updateBalance(it)
}
}
}
private suspend fun updateBalance(tx: WalletTransaction) {
val multiplier = when (tx.status) {
WalletTransactionStatus.SENT -> -1.0
WalletTransactionStatus.RECEIVED -> 1.0
}
existingBalance += tx.amount.multiply(BigDecimal(multiplier))
balanceChannel.send(existingBalance)
}
private fun checkForNewTransactions(): Set<WalletTransaction>? {
val count = notes.count()
if(count == existingTransactions.size) return null
val notes = notes.getAll().map { toWalletTransaction(it) }
return notes.subtract(existingTransactions)
}
private fun toWalletTransaction(note: NoteQuery): WalletTransaction {
return WalletTransaction(
height = note.height,
status = WalletTransactionStatus.RECEIVED,
amount = BigDecimal(note.value / 1e8),
timestamp = note.time
)
}
}

View File

@ -1,8 +1,7 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import android.text.format.DateUtils import android.text.format.DateUtils
import cash.z.android.wallet.vo.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.android.wallet.vo.WalletTransactionStatus
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce import kotlinx.coroutines.channels.produce
@ -11,6 +10,8 @@ import kotlinx.coroutines.isActive
import java.math.BigDecimal import java.math.BigDecimal
import kotlin.math.roundToLong import kotlin.math.roundToLong
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.random.nextLong
class SampleTransactionRepository(val scope: CoroutineScope) : TransactionRepository { class SampleTransactionRepository(val scope: CoroutineScope) : TransactionRepository {
/** /**
@ -32,34 +33,27 @@ class SampleTransactionRepository(val scope: CoroutineScope) : TransactionReposi
var oldestTimestamp = System.currentTimeMillis() - (4 * DateUtils.WEEK_IN_MILLIS) var oldestTimestamp = System.currentTimeMillis() - (4 * DateUtils.WEEK_IN_MILLIS)
while (isActive) { while (isActive) {
delay(1500L) delay(1500L)
send(createSampleTransaction(oldestTimestamp).also { oldestTimestamp = it.timestamp }) send(createSampleTransaction(oldestTimestamp).also { oldestTimestamp = it.timeInSeconds * 1000 })
} }
} }
private fun createSampleTransaction(): WalletTransaction { private fun createSampleTransaction(oldestTimestamp: Long): WalletTransaction {
return createSampleTransaction(System.currentTimeMillis() - (4 * DateUtils.WEEK_IN_MILLIS)) // up to 20% of the delta
} val upperBound = System.currentTimeMillis() + Math.round(0.2 * (System.currentTimeMillis() - oldestTimestamp))
val txId = Random.nextInt(0..(Int.MAX_VALUE - 1))
private fun createSampleTransaction(after: Long): WalletTransaction { val value = Random.nextLong(1L..1_500_000_000L) - 750_000_000L
val now = System.currentTimeMillis() val height = Random.nextInt(0..(Int.MAX_VALUE - 1))
val delta = now - after val isSend = value > 0L
val window = after + (0.05 * delta).roundToLong() val time = Random.nextLong(oldestTimestamp..upperBound)
val amount = BigDecimal(Random.nextDouble(0.1, 15.0) * arrayOf(-1, 1).random()) val isMined = Random.nextBoolean()
val status = if (amount > BigDecimal.ZERO) WalletTransactionStatus.SENT else WalletTransactionStatus.RECEIVED
val timestamp = Random.nextLong(after, window)
return WalletTransaction( return WalletTransaction(
timestamp.toInt(), txId = txId,
status, value = value,
timestamp, height = height,
amount isSend = isSend,
timeInSeconds = time/1000,
isMined = isMined
) )
} }
private fun createSampleTransactions(size: Int): MutableList<WalletTransaction> {
val transactions = mutableListOf<WalletTransaction>()
repeat(size) {
transactions.add(createSampleTransaction())
}
return transactions
}
} }

View File

@ -1,6 +1,6 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import cash.z.android.wallet.vo.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import java.math.BigDecimal import java.math.BigDecimal

View File

@ -1,5 +1,6 @@
package cash.z.android.wallet.di.module package cash.z.android.wallet.di.module
import android.util.Log
import cash.z.android.wallet.BuildConfig import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.ZcashWalletApplication import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.di.module.Properties.CACHE_DB_NAME import cash.z.android.wallet.di.module.Properties.CACHE_DB_NAME
@ -7,12 +8,17 @@ import cash.z.android.wallet.di.module.Properties.COMPACT_BLOCK_PORT
import cash.z.android.wallet.di.module.Properties.COMPACT_BLOCK_SERVER import cash.z.android.wallet.di.module.Properties.COMPACT_BLOCK_SERVER
import cash.z.android.wallet.di.module.Properties.DATA_DB_NAME import cash.z.android.wallet.di.module.Properties.DATA_DB_NAME
import cash.z.android.wallet.di.module.Properties.SEED_PROVIDER import cash.z.android.wallet.di.module.Properties.SEED_PROVIDER
import cash.z.android.wallet.di.module.Properties.SPENDING_KEY_PROVIDER
import cash.z.wallet.sdk.data.* import cash.z.wallet.sdk.data.*
import cash.z.wallet.sdk.jni.JniConverter import cash.z.wallet.sdk.jni.JniConverter
import cash.z.wallet.sdk.secure.Wallet import cash.z.wallet.sdk.secure.Wallet
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okio.ByteString
import java.nio.charset.Charset
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/** /**
* Module that contributes all the objects necessary for the synchronizer, which is basically everything that has * Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
@ -51,7 +57,7 @@ internal object SynchronizerModule {
@Provides @Provides
@Singleton @Singleton
fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet { fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet {
return Wallet(converter, application.getDatabasePath(DATA_DB_NAME).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SEED_PROVIDER, logger = twigger) return Wallet(converter, application.getDatabasePath(DATA_DB_NAME).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SEED_PROVIDER, spendingKeyProvider = SPENDING_KEY_PROVIDER, logger = twigger)
} }
@JvmStatic @JvmStatic
@ -64,7 +70,11 @@ internal object SynchronizerModule {
@JvmStatic @JvmStatic
@Provides @Provides
@Singleton @Singleton
fun provideJniConverter(): JniConverter = JniConverter() fun provideJniConverter(): JniConverter {
return JniConverter().also {
if (BuildConfig.DEBUG) it.initLogs()
}
}
@JvmStatic @JvmStatic
@Provides @Provides
@ -85,11 +95,12 @@ internal object SynchronizerModule {
// TODO: load most of these properties in later, perhaps from settings // TODO: load most of these properties in later, perhaps from settings
object Properties { object Properties {
val COMPACT_BLOCK_SERVER = Servers.ZCASH_TESTNET.host val COMPACT_BLOCK_SERVER = Servers.EMULATOR.host
const val COMPACT_BLOCK_PORT = 9067 const val COMPACT_BLOCK_PORT = 9067
const val CACHE_DB_NAME = "wallet_cache21.db" const val CACHE_DB_NAME = "wallet_cache50.db"
const val DATA_DB_NAME = "wallet_data21.db" const val DATA_DB_NAME = "wallet_data50.db"
val SEED_PROVIDER = SampleSeedProvider("dummyseed") val SEED_PROVIDER = SampleSeedProvider("dummyseed")
val SPENDING_KEY_PROVIDER = SampleSpendingKeyProvider("dummyseed")
} }
enum class Servers(val host: String) { enum class Servers(val host: String) {
@ -98,3 +109,52 @@ enum class Servers(val host: String) {
BOLT_TESTNET("ec2-34-228-10-162.compute-1.amazonaws.com"), BOLT_TESTNET("ec2-34-228-10-162.compute-1.amazonaws.com"),
ZCASH_TESTNET("lightwalletd.z.cash") ZCASH_TESTNET("lightwalletd.z.cash")
} }
class SampleImportedSeedProvider(private val seedHex: String) : ReadOnlyProperty<Any?, ByteArray> {
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
val bytes = ByteString.decodeHex(seedHex).toByteArray()
val stringBytes = String(bytes, Charset.forName("UTF-8"))
Log.e("TWIG-x", "byteString: $stringBytes")
return decodeHex(seedHex).also { Log.e("TWIG-x", "$it") }
}
fun decodeHex(hex: String): ByteArray {
val result = ByteArray(hex.length / 2)
for (i in result.indices) {
val d1 = decodeHexDigit(hex[i * 2]) shl 4
val d2 = decodeHexDigit(hex[i * 2 + 1])
result[i] = (d1 + d2).toByte()
}
return result
}
private fun decodeHexDigit(c: Char): Int {
if (c in '0'..'9') return c - '0'
if (c in 'a'..'f') return c - 'a' + 10
if (c in 'A'..'F') return c - 'A' + 10
throw IllegalArgumentException("Unexpected hex digit: $c")
}
}
class SampleSpendingKeyProvider2(private val seedValue: String) : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// dynamically generating keyes, based on seed is out of scope for this sample
return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9xke5cag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uuacghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5"
}
}

View File

@ -5,18 +5,16 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.android.wallet.R import cash.z.android.wallet.R
import cash.z.android.wallet.extention.toAppColor import cash.z.android.wallet.extention.toAppColor
import cash.z.android.wallet.vo.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import java.math.BigDecimal import cash.z.wallet.sdk.ext.toZec
import java.math.MathContext
import java.math.RoundingMode
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue
class TransactionAdapter : ListAdapter<WalletTransaction, TransactionViewHolder>(DIFF_CALLBACK) { class TransactionAdapter : ListAdapter<WalletTransaction, TransactionViewHolder>(DIFF_CALLBACK) {
@ -36,18 +34,18 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private val status = itemView.findViewById<View>(R.id.view_transaction_status) private val status = itemView.findViewById<View>(R.id.view_transaction_status)
private val timestamp = itemView.findViewById<TextView>(R.id.text_transaction_timestamp) private val timestamp = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
private val amount = itemView.findViewById<TextView>(R.id.text_transaction_amount) private val amount = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val background = itemView.findViewById<View>(R.id.container_transaction)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bind(tx: WalletTransaction) { fun bind(tx: WalletTransaction) {
val sign = if(tx.amount > BigDecimal.ZERO) "+" else "-" val sign = if(tx.isSend) "-" else "+"
val amountColor = if(tx.amount > BigDecimal.ZERO) R.color.colorPrimary else R.color.text_dark_dimmed val amountColor = if (tx.isSend) R.color.text_dark_dimmed else R.color.colorPrimary
status.setBackgroundColor(tx.status.color.toAppColor()) val transactionColor = if(tx.isSend) R.color.send_associated else R.color.receive_associated
timestamp.text = formatter.format(tx.timestamp) val zecAbsoluteValue = tx.value.absoluteValue.toZec(3)
amount.text = String.format("$sign %,.3f", tx.amount.round(MathContext(3, RoundingMode.HALF_EVEN )).abs()) status.setBackgroundColor(transactionColor.toAppColor())
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending" else formatter.format(tx.timeInSeconds * 1000)
Log.e("TWIG-z", "TimeInSeconds: ${tx.timeInSeconds}")
amount.text = "$sign$zecAbsoluteValue"
amount.setTextColor(amountColor.toAppColor()) amount.setTextColor(amountColor.toAppColor())
Log.e("TWIG-u", "formatted timestamp: ${tx.timestamp} for value ${amount.text}") }
}
} }

View File

@ -1,6 +1,5 @@
package cash.z.android.wallet.ui.fragment package cash.z.android.wallet.ui.fragment
import android.animation.Animator
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString import android.text.SpannableString
@ -10,16 +9,17 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import cash.z.android.wallet.R import cash.z.android.wallet.R
import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.extention.toAppColor import cash.z.android.wallet.extention.toAppColor
import cash.z.android.wallet.extention.toAppString import cash.z.android.wallet.extention.toAppString
import cash.z.android.wallet.extention.tryIgnore import cash.z.android.wallet.extention.tryIgnore
@ -28,22 +28,21 @@ import cash.z.android.wallet.ui.adapter.TransactionAdapter
import cash.z.android.wallet.ui.presenter.HomePresenter import cash.z.android.wallet.ui.presenter.HomePresenter
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
import cash.z.android.wallet.ui.util.TopAlignedSpan import cash.z.android.wallet.ui.util.TopAlignedSpan
import cash.z.android.wallet.vo.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.data.ActiveSendTransaction import cash.z.wallet.sdk.data.ActiveSendTransaction
import cash.z.wallet.sdk.data.ActiveTransaction import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.wallet.sdk.data.TransactionState import cash.z.wallet.sdk.data.TransactionState
import cash.z.wallet.sdk.ext.toZec import cash.z.wallet.sdk.ext.toZec
import com.google.android.material.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialActionItem
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.activity_main_first_run.*
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.include_home_content.* import kotlinx.android.synthetic.main.include_home_content.*
import kotlinx.android.synthetic.main.include_home_header.* import kotlinx.android.synthetic.main.include_home_header.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_first_run.*
import kotlin.random.nextLong import kotlin.random.nextLong
@ -56,6 +55,7 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
lateinit var homePresenter: HomePresenter lateinit var homePresenter: HomePresenter
lateinit var transactionAdapter: TransactionAdapter lateinit var transactionAdapter: TransactionAdapter
private var viewsInitialized = false private var viewsInitialized = false
private var snackbar: Snackbar? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -97,15 +97,16 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
// forceRedraw() // forceRedraw()
// toggleViews(false) // toggleViews(false)
} else {
forceRedraw()
toggleViews(false)
} }
} }
button_active_transaction_cancel.setOnClickListener { button_active_transaction_cancel.setOnClickListener {
onCancelActiveTransaction() val transaction = button_active_transaction_cancel.tag as? ActiveSendTransaction
if (transaction != null) {
homePresenter.onCancelActiveTransaction(transaction)
} else {
Toaster.short("Error: unable to find transaction to cancel!")
}
} }
refresh_layout.setOnRefreshListener { refresh_layout.setOnRefreshListener {
@ -167,6 +168,7 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
// //
// View API // View API
// //
//TODO: pull some of this logic into the presenter, particularly the part that deals with ZEC <-> USD price conversion //TODO: pull some of this logic into the presenter, particularly the part that deals with ZEC <-> USD price conversion
override fun updateBalance(old: Long, new: Long) { override fun updateBalance(old: Long, new: Long) {
//TODO: remove this kind of thing //TODO: remove this kind of thing
@ -184,18 +186,19 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
override fun setTransactions(transactions: List<WalletTransaction>) { override fun setTransactions(transactions: List<WalletTransaction>) {
Log.e("TWIG-t", "submitList called with ${transactions.size} transactions") Log.e("TWIG-t", "submitList called with ${transactions.size} transactions")
transactionAdapter.submitList(transactions) transactionAdapter.submitList(transactions)
recycler_transactions.postDelayed({
recycler_transactions.smoothScrollToPosition(0) recycler_transactions.smoothScrollToPosition(0)
}, 100L)
if (transactions.isNotEmpty()) setFirstRunShown(false) if (transactions.isNotEmpty()) setFirstRunShown(false)
} }
var snackbar: Snackbar? = null
override fun showProgress(progress: Int) { override fun showProgress(progress: Int) {
Log.e("TWIG", "showing progress of $progress") Log.e("TWIG", "showing progress of $progress")
// TODO: improve this with Lottie animation. but for now just use the empty view for downloading... // TODO: improve this with Lottie animation. but for now just use the empty view for downloading...
// var hasEmptyViews = group_empty_view_items.visibility == View.VISIBLE // var hasEmptyViews = group_empty_view_items.visibility == View.VISIBLE
// if(!viewsInitialized) toggleViews(true) // if(!viewsInitialized) toggleViews(true)
// //
val message = if(progress >= 100) "Download complete! Processing...\n(this may take a while)" else "Downloading blocks ($progress%)" val message = if(progress >= 100) "Download complete! Processing blocks..." else "Downloading blocks ($progress%)"
// text_wallet_message.text = message // text_wallet_message.text = message
if (snackbar == null && progress <= 50) { if (snackbar == null && progress <= 50) {
@ -210,6 +213,19 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
} }
} }
fun showOkSnack(message: String) {
if (snackbar == null) {
snackbar = Snackbar.make(view!!, "$message", Snackbar.LENGTH_INDEFINITE).setAction("OK") {
snackbar?.dismiss()
snackbar = null
}
snackbar?.show()
} else {
snackbar?.setText(message)
if (snackbar?.isShownOrQueued != true) snackbar?.show()
}
}
override fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>) { override fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>) {
if (activeTransactionMap.isEmpty()) { if (activeTransactionMap.isEmpty()) {
setActiveTransactionsShown(false) setActiveTransactionsShown(false)
@ -224,7 +240,12 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
// TODO: update remaining transactions // TODO: update remaining transactions
} }
override fun onCancelledTooLate() {
showOkSnack("Oops! It was too late to cancel!")
}
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) { private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
Log.e("TWIG", "setting transaction state to ${transactionState::class.simpleName}")
var title = "Active Transaction" var title = "Active Transaction"
var subtitle = "Processing..." var subtitle = "Processing..."
when (transactionState) { when (transactionState) {
@ -232,13 +253,14 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
header_active_transaction.visibility = View.VISIBLE header_active_transaction.visibility = View.VISIBLE
title = "Preparing ${transaction.value.toZec(3)} ZEC" title = "Preparing ${transaction.value.toZec(3)} ZEC"
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}" subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}"
button_active_transaction_cancel.text = "cancel" setTransactionActive(transaction, true)
setActiveTransactionRaised(true)
} }
TransactionState.SendingToNetwork -> { TransactionState.SendingToNetwork -> {
title = "Sending Transaction" title = "Sending Transaction"
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}" subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}"
button_active_transaction_cancel.text = "${transaction.value/1000L}" text_active_transaction_value.text = "${transaction.value/1000L}"
text_active_transaction_value.visibility = View.VISIBLE
button_active_transaction_cancel.visibility = View.GONE
} }
is TransactionState.Failure -> { is TransactionState.Failure -> {
lottie_active_transaction.setAnimation(R.raw.lottie_send_failure) lottie_active_transaction.setAnimation(R.raw.lottie_send_failure)
@ -250,18 +272,27 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
else -> "Unrecoginzed error" else -> "Unrecoginzed error"
} }
button_active_transaction_cancel.visibility = View.GONE button_active_transaction_cancel.visibility = View.GONE
onCancelActiveTransaction() text_active_transaction_value.visibility = View.GONE
setTransactionActive(transaction, false)
} }
is TransactionState.AwaitingConfirmations -> { is TransactionState.AwaitingConfirmations -> {
if (transactionState.confirmationCount < 1) { if (transactionState.confirmationCount < 1) {
lottie_active_transaction.setAnimation(R.raw.lottie_send_success) lottie_active_transaction.setAnimation(R.raw.lottie_send_success)
lottie_active_transaction.playAnimation() lottie_active_transaction.playAnimation()
title = "ZEC Sent" title = "ZEC Sent"
subtitle = "Awaiting Confirmations (${transactionState.confirmationCount}/10)" subtitle = "Today at 4:46pm"
text_active_transaction_value.text = transaction.value.toZec(3).toString()
text_active_transaction_value.visibility = View.VISIBLE
button_active_transaction_cancel.visibility = View.GONE
} else { } else {
// play confirmation counting animation // play confirmation counting animation
} }
} }
is TransactionState.Cancelled -> {
title = text_active_transaction_title.text.toString()
subtitle = text_active_transaction_subtitle.text.toString()
setTransactionActive(transaction, false)
}
} }
text_active_transaction_title.text = title text_active_transaction_title.text = title
text_active_transaction_subtitle.text = subtitle text_active_transaction_subtitle.text = subtitle
@ -347,6 +378,7 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
} }
private fun onActiveTransactionTransitionEnd() { private fun onActiveTransactionTransitionEnd() {
// TODO: investigate if this fix is still required after getting transition animation working again
// fixes a bug where the translation gets lost, during animation. As a nice side effect, visually, it makes the view appear to settle in to position // fixes a bug where the translation gets lost, during animation. As a nice side effect, visually, it makes the view appear to settle in to position
header_active_transaction.translationZ = 10.0f header_active_transaction.translationZ = 10.0f
button_active_transaction_cancel.apply { button_active_transaction_cancel.apply {
@ -355,38 +387,30 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
} }
} }
private fun onCancelActiveTransaction() { private fun setTransactionActive(transaction: ActiveTransaction, isActive: Boolean) {
setActiveTransactionRaised(false) // TODO: get view for transaction, mostly likely keep a sparse array of these or something
button_active_transaction_cancel.text = "cancel" if (isActive) {
} button_active_transaction_cancel.setText(R.string.cancel)
button_active_transaction_cancel.isEnabled = true
private fun setActiveTransactionRaised(isRaised: Boolean) { button_active_transaction_cancel.tag = transaction
button_active_transaction_cancel.isEnabled = isRaised
header_active_transaction.animate().apply { header_active_transaction.animate().apply {
translationZ(if (isRaised) 10f else 0f) translationZ(10f)
duration = 200L duration = 200L
interpolator = DecelerateInterpolator()
}
} else {
button_active_transaction_cancel.setText(R.string.cancelled)
button_active_transaction_cancel.isEnabled = false
button_active_transaction_cancel.tag = null
header_active_transaction.animate().apply {
translationZ(2f)
duration = 300L
interpolator = AccelerateInterpolator() interpolator = AccelerateInterpolator()
setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
} }
lottie_active_transaction.cancelAnimation()
override fun onAnimationEnd(animation: Animator?) {
header_active_transaction.apply {
if(translationZ == 0f) setBackgroundResource(0)
} }
} }
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
} )
}
}
inner class HomeTransitionListener : Transition.TransitionListener { inner class HomeTransitionListener : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) { override fun onTransitionStart(transition: Transition) {
onActiveTransactionTransitionStart() onActiveTransactionTransitionStart()

View File

@ -13,6 +13,7 @@ import cash.z.android.wallet.R
import cash.z.android.wallet.ui.activity.MainActivity import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.util.AddressPartNumberSpan import cash.z.android.wallet.ui.util.AddressPartNumberSpan
import cash.z.wallet.sdk.jni.JniConverter import cash.z.wallet.sdk.jni.JniConverter
import cash.z.wallet.sdk.secure.Wallet
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.fragment_receive.* import kotlinx.android.synthetic.main.fragment_receive.*
@ -27,7 +28,7 @@ class ReceiveFragment : BaseFragment() {
lateinit var qrecycler: QRecycler lateinit var qrecycler: QRecycler
@Inject @Inject
lateinit var converter: JniConverter lateinit var wallet: Wallet
lateinit var addressParts: Array<TextView> lateinit var addressParts: Array<TextView>
@ -87,7 +88,7 @@ class ReceiveFragment : BaseFragment() {
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB // TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
private fun loadAddress(): String { private fun loadAddress(): String {
return converter.getAddress("dummyseed".toByteArray()) return wallet.getAddress()
} }
} }

View File

@ -8,6 +8,9 @@ import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
@ -21,6 +24,7 @@ import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.presenter.SendPresenter import cash.z.android.wallet.ui.presenter.SendPresenter
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.DecimalFormat import java.text.DecimalFormat
@ -195,6 +199,10 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
// //
private fun showSendDialog() { private fun showSendDialog() {
// hide soft keyboard
mainActivity.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
setSendEnabled(false) // partially because we need to lower the button elevation setSendEnabled(false) // partially because we need to lower the button elevation
binding.groupDialogSend.visibility = View.VISIBLE binding.groupDialogSend.visibility = View.VISIBLE
} }
@ -208,10 +216,10 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
binding.buttonSendZec.isEnabled = isEnabled binding.buttonSendZec.isEnabled = isEnabled
if (isEnabled) { if (isEnabled) {
binding.buttonSendZec.text = "send zec" binding.buttonSendZec.text = "send zec"
binding.progressSend.visibility = View.GONE // binding.progressSend.visibility = View.GONE
} else { } else {
binding.buttonSendZec.text = "sending..." binding.buttonSendZec.text = "sending..."
binding.progressSend.visibility = View.VISIBLE // binding.progressSend.visibility = View.VISIBLE
} }
} }
} }

View File

@ -1,19 +1,15 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import android.util.Log import android.util.Log
import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.android.wallet.vo.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.android.wallet.vo.WalletTransactionStatus.RECEIVED import cash.z.wallet.sdk.data.ActiveSendTransaction
import cash.z.android.wallet.vo.WalletTransactionStatus.SENT
import cash.z.wallet.sdk.data.ActiveTransaction import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.TransactionState import cash.z.wallet.sdk.data.TransactionState
import cash.z.wallet.sdk.vo.NoteQuery
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import java.math.BigDecimal
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class HomePresenter( class HomePresenter(
@ -29,6 +25,7 @@ class HomePresenter(
fun updateBalance(old: Long, new: Long) fun updateBalance(old: Long, new: Long)
fun showProgress(progress: Int) fun showProgress(progress: Int)
fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>) fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>)
fun onCancelledTooLate()
} }
override suspend fun start() { override suspend fun start() {
@ -54,20 +51,15 @@ class HomePresenter(
Log.e("@TWIG", "balance binder exiting!") Log.e("@TWIG", "balance binder exiting!")
} }
private fun CoroutineScope.launchTransactionBinder(channel: ReceiveChannel<List<NoteQuery>>) = launch { private fun CoroutineScope.launchTransactionBinder(channel: ReceiveChannel<List<WalletTransaction>>) = launch {
Log.e("@TWIG", "transaction binder starting!") Log.e("@TWIG", "transaction binder starting!")
for (noteQueryList in channel) { for (walletTransactionList in channel) {
Log.e("@TWIG", "received ${noteQueryList.size} transactions for presenting") Log.e("@TWIG", "received ${walletTransactionList.size} transactions for presenting")
bind(noteQueryList.map { bind(walletTransactionList)
val time = updateTimeStamp(it)
it.toWalletTransaction(time)
})
} }
Log.e("@TWIG", "transaction binder exiting!") Log.e("@TWIG", "transaction binder exiting!")
} }
private suspend fun updateTimeStamp(noteQuery: NoteQuery) = synchronizer.updateTimeStamp(noteQuery.height)
private fun CoroutineScope.launchProgressMonitor(channel: ReceiveChannel<Int>) = launch { private fun CoroutineScope.launchProgressMonitor(channel: ReceiveChannel<Int>) = launch {
Log.e("@TWIG", "progress monitor starting on thread ${Thread.currentThread().name}!") Log.e("@TWIG", "progress monitor starting on thread ${Thread.currentThread().name}!")
for (i in channel) { for (i in channel) {
@ -109,9 +101,12 @@ class HomePresenter(
if (activeTransactionMap.isNotEmpty()) view.setActiveTransactions(activeTransactionMap) if (activeTransactionMap.isNotEmpty()) view.setActiveTransactions(activeTransactionMap)
} }
fun onCancelActiveTransaction() { fun onCancelActiveTransaction(transaction: ActiveSendTransaction) {
// TODO: hold a reference to the job and cancel it Log.e("@TWIG", "requesting to cancel send for transaction ${transaction.internalId}")
Toaster.short("Cancelled transaction!") val isTooLate = !synchronizer.cancelSend(transaction)
if (isTooLate) {
view.onCancelledTooLate()
}
} }
private fun onMain(block: () -> Unit) = launch { private fun onMain(block: () -> Unit) = launch {
@ -122,11 +117,5 @@ class HomePresenter(
} }
} }
private fun NoteQuery.toWalletTransaction(timeOverride: Long? = null): WalletTransaction {
// convert time from seconds to milliseconds
val timestamp = if (timeOverride == null) time * 1000 else timeOverride * 1000
Log.e("@TWIG-u", "setting timestamp to $timestamp for value $value")
return WalletTransaction(height, if (sent) SENT else RECEIVED, timestamp, BigDecimal(value / 1e8))
}
} }

View File

@ -3,7 +3,7 @@ package cash.z.android.wallet.ui.presenter
import android.util.Log import android.util.Log
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.vo.Transaction import cash.z.wallet.sdk.entity.Transaction
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job

View File

@ -2,6 +2,7 @@ package cash.z.android.wallet.ui.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
@ -11,6 +12,7 @@ class CollapsingMotionToolbar @JvmOverloads constructor(
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
progress = -verticalOffset / appBarLayout.totalScrollRange.toFloat() progress = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
Log.e("MotionL", "progress: $progress verticalOffset: $verticalOffset scrollRange: ${appBarLayout.totalScrollRange.toFloat()}")
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {

View File

@ -1,12 +0,0 @@
package cash.z.android.wallet.vo
import cash.z.android.wallet.R
import androidx.annotation.ColorRes
import java.math.BigDecimal
data class WalletTransaction(val height: Int, val status: WalletTransactionStatus, val timestamp: Long, val amount: BigDecimal)
enum class WalletTransactionStatus(@ColorRes val color: Int) {
SENT(R.color.colorPrimary),
RECEIVED(R.color.colorAccent);
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="2dp" />
<solid android:color="@color/zcashWhite" />
</shape>

View File

@ -29,9 +29,14 @@
android:id="@+id/header_active_transaction" android:id="@+id/header_active_transaction"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="70dp" android:layout_height="70dp"
android:background="@drawable/background_rounded_corners_opaque" android:background="@drawable/background_rounded_corners_slight"
android:clipToPadding="false" android:clipToPadding="false"
android:padding="16dp" android:paddingStart="16dp"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:paddingBottom="16dp"
android:transitionName="@string/transition_active_transaction_card" android:transitionName="@string/transition_active_transaction_card"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -57,8 +62,10 @@
android:textSize="@dimen/text_size_body_1" android:textSize="@dimen/text_size_body_1"
android:textStyle="bold" android:textStyle="bold"
android:transitionName="@string/transition_active_transaction_title" android:transitionName="@string/transition_active_transaction_title"
app:layout_constraintBottom_toTopOf="@id/text_active_transaction_subtitle"
app:layout_constraintStart_toEndOf="@id/lottie_active_transaction" app:layout_constraintStart_toEndOf="@id/lottie_active_transaction"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView <TextView
android:id="@+id/text_active_transaction_subtitle" android:id="@+id/text_active_transaction_subtitle"
@ -67,9 +74,23 @@
android:text="to zsapling...123456789" android:text="to zsapling...123456789"
android:textSize="@dimen/text_size_caption" android:textSize="@dimen/text_size_caption"
android:transitionName="@string/transition_active_transaction_address" android:transitionName="@string/transition_active_transaction_address"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/text_active_transaction_title" app:layout_constraintStart_toStartOf="@id/text_active_transaction_title"
app:layout_constraintTop_toBottomOf="@id/text_active_transaction_title" /> app:layout_constraintTop_toBottomOf="@id/text_active_transaction_title" />
<TextView
android:id="@+id/text_active_transaction_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="@color/text_dark_dimmed"
android:textSize="@dimen/text_size_h5"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button <Button
android:id="@+id/button_active_transaction_cancel" android:id="@+id/button_active_transaction_cancel"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -40,10 +40,15 @@
<!-- every color here should be a reference to a palette color but have a more useful name for use in code --> <!-- every color here should be a reference to a palette color but have a more useful name for use in code -->
<!-- association -->
<color name="send_associated">@color/colorAccent</color>
<color name="receive_associated">@color/colorPrimary</color> <!-- design decision: when you get money it should be associated with green -->
<color name="request_associated">@color/zcashBlue</color>
<!-- icons --> <!-- icons -->
<color name="icon_send">@color/colorPrimary</color> <color name="icon_send">@color/send_associated</color>
<color name="icon_receive">@color/colorAccent</color> <color name="icon_receive">@color/receive_associated</color>
<color name="icon_request">@color/zcashBlue</color> <color name="icon_request">@color/request_associated</color>
<color name="fab_icon_color">@color/zcashWhite</color> <color name="fab_icon_color">@color/zcashWhite</color>
<color name="fab_closed_color">@color/colorPrimary</color> <color name="fab_closed_color">@color/colorPrimary</color>
<color name="fab_open_color">@color/colorPrimaryDark</color> <color name="fab_open_color">@color/colorPrimaryDark</color>

View File

@ -7,6 +7,10 @@
<string name="nav_header_desc">Navigation header</string> <string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<!-- General -->
<string name="cancel">cancel</string>
<string name="cancelled">cancelled</string>
<!-- Destinations --> <!-- Destinations -->
<string name="destination_title_home" /> <string name="destination_title_home" />
<string name="destination_title_send">Send Zcash</string> <string name="destination_title_send">Send Zcash</string>

View File

@ -13,9 +13,10 @@ buildscript {
ext.deps = [ ext.deps = [
'androidx': [ 'androidx': [
'appcompat': 'androidx.appcompat:appcompat:1.0.0', 'appcompat': 'androidx.appcompat:appcompat:1.0.0',
'constraintLayout': 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2', 'constraintLayout': 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3',
'core': 'androidx.core:core:1.0.0', 'core': 'androidx.core:core:1.0.0',
'coreKtx': 'androidx.core:core-ktx:1.0.0', 'coreKtx': 'androidx.core:core-ktx:1.0.0',
'multidex': 'androidx.multidex:multidex:2.0.1',
'navigation': [ 'navigation': [
'fragment': "android.arch.navigation:navigation-fragment:${versions.navigation}", 'fragment': "android.arch.navigation:navigation-fragment:${versions.navigation}",
'fragmentKtx': "android.arch.navigation:navigation-fragment-ktx:${versions.navigation}", 'fragmentKtx': "android.arch.navigation:navigation-fragment-ktx:${versions.navigation}",
@ -40,12 +41,15 @@ buildscript {
'core': "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", 'core': "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}",
'android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}" 'android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
], ],
'reflect': "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}",
'stdlib': "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" 'stdlib': "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
], ],
'lottie': "com.airbnb.android:lottie:2.7.0",
'material': 'com.google.android.material:material:1.0.0', 'material': 'com.google.android.material:material:1.0.0',
'speeddial': 'com.leinardi.android:speed-dial:2.0.0', 'speeddial': 'com.leinardi.android:speed-dial:2.0.0',
'stetho': 'com.facebook.stetho:stetho:1.5.0',
'zcash': [ 'zcash': [
'walletSdk': "cash.z.android.wallet:zcash-android-wallet-sdk:1.4.0@aar" 'walletSdk': "cash.z.android.wallet:zcash-android-wallet-sdk:1.6.0@aar"
] ]
] ]
repositories { repositories {