Merge pull request #62 from zcash/feature/profile-screen

Feature/profile screen
This commit is contained in:
Kevin Gorham 2020-01-09 11:07:24 -05:00 committed by GitHub
commit cb9e6cc4b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 884 additions and 224 deletions

View File

@ -9,7 +9,7 @@ apply plugin: 'com.google.gms.google-services'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha04'
version = '1.0.0-alpha05'
android {
compileSdkVersion Deps.compileSdkVersion
@ -19,7 +19,7 @@ android {
applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_004
versionCode = 1_00_00_005
// last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionName = "$version"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@ -7,13 +7,21 @@ import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraXConfig
import cash.z.ecc.android.di.component.AppComponent
import cash.z.ecc.android.di.component.DaggerAppComponent
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Provider
class ZcashWalletApp : Application(), CameraXConfig.Provider {
@Inject
lateinit var feedbackCoordinator: FeedbackCoordinator
var creationTime: Long = 0
private set
@ -26,7 +34,8 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
super.onCreate()
component = DaggerAppComponent.factory().create(this)
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
component.inject(this)
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(feedbackCoordinator, Thread.getDefaultUncaughtExceptionHandler()))
Twig.plant(TroubleshootingTwig())
}
@ -44,11 +53,21 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
lateinit var component: AppComponent
}
class ExceptionReporter(val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
/**
* @param feedbackCoordinator inject a provider so that if a crash happens before configuration
* is complete, we can lazily initialize all the feedback objects at this moment so that we
* don't have to add any time to startup.
*/
class ExceptionReporter(private val coordinator: FeedbackCoordinator, private val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread?, e: Throwable?) {
// trackCrash(e, "Top-level exception wasn't caught by anything else!")
// Analytics.clear()
twig("Uncaught Exception: $e")
coordinator.feedback.report(e)
coordinator.flush()
// can do this if necessary but first verify that we need it
runBlocking {
coordinator.await()
coordinator.feedback.stop()
}
ogHandler.uncaughtException(t, e)
}
}

View File

@ -9,6 +9,8 @@ import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(zcashWalletApp: ZcashWalletApp)
// Subcomponents
fun mainActivitySubcomponent(): MainActivitySubcomponent.Factory
fun synchronizerSubcomponent(): SynchronizerSubcomponent.Factory

View File

@ -3,13 +3,11 @@ package cash.z.ecc.android.di.module
import android.content.ClipboardManager
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.wallet.sdk.Initializer
import cash.z.ecc.android.feedback.*
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.multibindings.IntoSet
import javax.inject.Singleton
@Module(subcomponents = [MainActivitySubcomponent::class])
@ -23,4 +21,40 @@ class AppModule {
@Singleton
fun provideClipboard(context: Context) =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
//
// Feedback
//
@Provides
@Singleton
fun provideFeedback(): Feedback = Feedback()
@Provides
@Singleton
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@Singleton
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@Singleton
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@Singleton
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -1,44 +1,10 @@
package cash.z.ecc.android.di.module
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.*
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
@Module(includes = [ViewModelsActivityModule::class], subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class])
class MainActivityModule {
@Provides
@ActivityScope
fun provideFeedback(): Feedback = Feedback()
@Provides
@ActivityScope
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -0,0 +1,13 @@
package cash.z.ecc.android.ext
import android.text.Spannable
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.toSpannable
fun String.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable {
return toSpannable().apply {
val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

View File

@ -13,8 +13,10 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
@ -149,7 +151,7 @@ class MainActivity : AppCompatActivity() {
}
}
fun copyAddress(view: View) {
fun copyAddress(view: View? = null) {
lifecycleScope.launch {
clipboard.setPrimaryClip(
ClipData.newPlainText(
@ -161,6 +163,18 @@ class MainActivity : AppCompatActivity() {
}
}
fun preventBackPress(fragment: Fragment) {
onFragmentBackPressed(fragment){}
}
fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) {
onBackPressedDispatcher.addCallback(fragment, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
}
})
}
private fun showMessage(message: String, action: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

View File

@ -9,10 +9,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.*
abstract class BaseFragment<T : ViewBinding> : Fragment() {
val mainActivity: MainActivity? get() = activity as MainActivity?
@ -33,7 +30,7 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
override fun onResume() {
super.onResume()
resumedScope = lifecycleScope.coroutineContext.let {
CoroutineScope(it + SupervisorJob(it[Job]))
CoroutineScope(Dispatchers.Main + SupervisorJob(it[Job]))
}
}
@ -45,4 +42,10 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
// inflate is static in the ViewBinding class so we can't handle this ourselves
// each fragment must call FragmentMyLayoutBinding.inflate(inflater)
abstract fun inflate(@NonNull inflater: LayoutInflater): T
fun onBackPressNavTo(navResId: Int) {
mainActivity?.onFragmentBackPressed(this) {
mainActivity?.navController?.navigate(navResId)
}
}
}

View File

@ -6,7 +6,7 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat
import java.util.*
@ -23,7 +23,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
var lineTwo: String = ""
var amount: String = ""
var amountColor: Int = 0
var indicatorColor: Int = 0
var indicatorBackground: Int = 0
transaction?.apply {
amount = value.convertZatoshiToZecString()
@ -32,18 +32,18 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
val isMined = blockTimeInSeconds != 0L
when {
!toAddress.isNullOrEmpty() -> {
lineOne = "You paid ${toAddress?.abbreviatedAddress()}"
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
amount = "- $amount"
amountColor = R.color.zcashRed
indicatorColor = R.color.colorPrimary
indicatorBackground = R.drawable.background_indicator_outbound
}
raw == null || raw?.isEmpty() == true -> {
lineOne = "Unknown paid you"
lineTwo = "Received $timestamp"
amount = "+ $amount"
amountColor = R.color.zcashGreen
indicatorColor = R.color.zcashGreen
indicatorBackground = R.drawable.background_indicator_inbound
}
else -> {
lineOne = "Unknown"
@ -56,6 +56,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
bottomText.text = lineTwo
amountText.text = amount
amountText.setTextColor(amountColor.toAppColor())
indicator.setBackgroundColor(indicatorColor.toAppColor())
val context = itemView.context
indicator.background = context.resources.getDrawable(indicatorBackground)
}
}

View File

@ -1,22 +1,25 @@
package cash.z.ecc.android.ui.detail
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentDetailBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.collectWith
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import cash.z.wallet.sdk.ext.twig
import okio.Okio
import kotlinx.coroutines.launch
class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
@ -31,21 +34,26 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.backButtonHitArea.onClickNavUp()
onClick(binding.buttonFeedback) {
onSendFeedback()
}
onClick(binding.buttonLogs) {
onViewLogs()
}
onClick(binding.buttonBackup, 1L) {
onBackupWallet()
lifecycleScope.launch {
binding.textAddress.text = viewModel.getAddress().toAbbreviatedAddress()
}
}
override fun onResume() {
super.onResume()
initTransactionUI()
viewModel.balance.collectWith(resumedScope) {
onBalanceUpdated(it)
}
}
private fun onBalanceUpdated(balance: WalletBalance) {
binding.textBalanceAvailable.text = balance.availableZatoshi.convertZatoshiToZecString()
val change = balance.totalZatoshi - balance.availableZatoshi
binding.textBalanceDescription.apply {
goneIf(change <= 0)
text = "(expecting +$change ZEC in change)".toColoredSpan(R.color.text_light, "+$change")
}
}
private fun initTransactionUI() {
@ -56,41 +64,8 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
binding.recyclerTransactions.adapter = adapter
}
private fun onSendFeedback() {
mainActivity?.showSnackbar("Feedback not yet implemented.")
}
private fun onViewLogs() {
loadLogFileAsText().let { logText ->
if (logText == null) {
mainActivity?.showSnackbar("Log file not found!")
} else {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, logText)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, "Share Log File")
startActivity(shareIntent)
}
}
}
private fun onBackupWallet() {
mainActivity?.navController?.navigate(R.id.action_nav_detail_to_backup_wallet)
}
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
twig("got a new paged list of transactions")
adapter.submitList(transactions)
}
private fun loadLogFileAsText(): String? {
val feedbackFile: FeedbackFile =
mainActivity?.feedbackCoordinator?.findObserver() ?: return null
Okio.buffer(Okio.source(feedbackFile.file)).use {
return it.readUtf8()
}
}
}

View File

@ -11,6 +11,9 @@ class WalletDetailViewModel @Inject constructor() : ViewModel() {
lateinit var synchronizer: Synchronizer
val transactions get() = synchronizer.clearedTransactions
val balance get() = synchronizer.balances
suspend fun getAddress() = synchronizer.getAddress()
override fun onCleared() {
super.onCleared()

View File

@ -3,17 +3,19 @@ package cash.z.ecc.android.ui.home
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.core.text.toSpannable
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.disabledIf
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.SendViewModel
@ -87,7 +89,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
buttonNumberPadDecimal.asKey(),
buttonNumberPadBack.asKey()
)
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive)
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile)
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
hitAreaScan.onClickNavTo(R.id.action_nav_home_to_nav_scan)
@ -98,17 +100,33 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
buttonSend.setOnClickListener {
onSend()
}
setSendAmount("0")
}
binding.buttonNumberPadBack.setOnLongClickListener {
onClearAmount()
true
}
// if (::uiModel.isInitialized) {
// twig("uiModel exists!")
// onModelUpdated(HomeViewModel.UiModel(), uiModel)
// }
}
private fun onClearAmount() {
repeat(binding.textSendAmount.text.length) {
resumedScope.launch {
_typedChars.send('<')
}
}
}
override fun onResume() {
super.onResume()
viewModel.initialize(typedChars)
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
onClearAmount()
viewModel.uiModels.scanReduce { old, new ->
onModelUpdated(old, new)
new
@ -163,8 +181,11 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
}
/**
* @param amount the amount to send represented as ZEC, without the dollar sign.
*/
fun setSendAmount(amount: String) {
binding.textSendAmount.text = "\$$amount"
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
binding.buttonSend.disabledIf(amount == "0")
}
@ -175,7 +196,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
binding.textBalanceDescription.apply {
goneIf(availableBalance < 0)
text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
"(expecting +${(totalBalance - availableBalance).convertZatoshiToZecString()} ZEC in change)"
val change = (totalBalance - availableBalance).convertZatoshiToZecString()
"(expecting +$change ZEC in change)".toColoredSpan(R.color.text_light, "+$change")
} else {
"(enter an amount to send)"
}
@ -221,7 +243,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
private fun onSynced(uiModel: HomeViewModel.UiModel) {
if (!uiModel.hasFunds) {
if (!uiModel.hasBalance) {
onNoFunds()
} else {
setBanner("")

View File

@ -57,7 +57,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
uiModels = synchronizer.run {
combine(status, progress, balances, zec) { s, p, b, z->
UiModel(s, p, b.available, b.total, z)
UiModel(s, p, b.availableZatoshi, b.totalZatoshi, z)
}
}.conflate()
}
@ -81,6 +81,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
): Parcelable {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds: Boolean get() = availableBalance > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.0001
val hasBalance: Boolean get() = totalBalance > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.0001
val isSynced: Boolean get() = status == SYNCED
val isSendEnabled: Boolean get() = isSynced && hasFunds
}

View File

@ -0,0 +1,62 @@
package cash.z.ecc.android.ui.profile
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentProfileBinding
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.ui.base.BaseFragment
import okio.Okio
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentProfileBinding =
FragmentProfileBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaClose.onClickNavBack()
binding.buttonBackup.onClickNavTo(R.id.action_nav_profile_to_nav_backup)
binding.textVersion.text = BuildConfig.VERSION_NAME
onClick(binding.buttonLogs) {
onViewLogs()
}
onClick(binding.buttonFeedback) {
onSendFeedback()
}
}
private fun onViewLogs() {
loadLogFileAsText().let { logText ->
if (logText == null) {
mainActivity?.showSnackbar("Log file not found!")
} else {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, logText)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, "Share Log File")
startActivity(shareIntent)
}
}
}
private fun onSendFeedback() {
mainActivity?.showSnackbar("Feedback feature coming soon!")
}
private fun loadLogFileAsText(): String? {
val feedbackFile: FeedbackFile =
mainActivity?.feedbackCoordinator?.findObserver() ?: return null
Okio.buffer(Okio.source(feedbackFile.file)).use {
return it.readUtf8()
}
}
}

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentReceiveNewBinding
@ -12,7 +11,7 @@ import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@ -63,7 +62,7 @@ class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() {
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
.into(binding.receiveQrCode)
binding.receiveAddress.text = address.abbreviatedAddress(12, 12)
binding.receiveAddress.text = address.toAbbreviatedAddress(12, 12)
// address.distribute(8) { i, part ->
// setAddressPart(i, part)

View File

@ -13,9 +13,6 @@ import cash.z.ecc.android.ext.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onErrorResumeNext
import kotlinx.coroutines.launch
class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
@ -31,7 +28,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
binding.buttonNext.setOnClickListener {
onSubmit()
}
binding.backButtonHitArea.onClickNavBack()
binding.backButtonHitArea.onClickNavTo(R.id.action_nav_send_address_to_nav_home)
binding.textBannerAction.setOnClickListener {
onPaste()
}
@ -49,7 +46,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
binding.inputZcashAmount.setText(null)
}
if (!sendViewModel.toAddress.isNullOrEmpty()){
binding.textAmount.text = "Send to ${sendViewModel.toAddress.abbreviatedAddress()}"
binding.textAmount.text = "Send to ${sendViewModel.toAddress.toAbbreviatedAddress()}"
binding.inputZcashAddress.setText(sendViewModel.toAddress)
} else {
binding.inputZcashAddress.setText(null)

View File

@ -7,11 +7,11 @@ import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import kotlinx.coroutines.launch
@ -27,10 +27,13 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
binding.buttonNext.setOnClickListener {
onSend()
}
binding.backButtonHitArea.onClickNavBack()
R.id.action_nav_send_confirm_to_nav_send_memo.let {
binding.backButtonHitArea.onClickNavTo(it)
onBackPressNavTo(it)
}
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.abbreviatedAddress()}?"
"Send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.toAbbreviatedAddress()}?"
}
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo

View File

@ -8,11 +8,10 @@ import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.entity.*
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.delay
@ -37,11 +36,12 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
onExit()
}
binding.textConfirmation.text =
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel.toAddress.abbreviatedAddress()}"
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel.toAddress.toAbbreviatedAddress()}"
sendViewModel.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
mainActivity?.preventBackPress(this)
}
override fun onAttach(context: Context) {
@ -91,6 +91,6 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
}
private fun onExit() {
mainActivity?.navController?.popBackStack(R.id.send_navigation, true)
mainActivity?.navController?.popBackStack(R.id.nav_send_address, true)
}
}

View File

@ -7,8 +7,7 @@ import android.view.inputmethod.EditorInfo
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ui.base.BaseFragment
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
@ -28,7 +27,10 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
sendViewModel.memo = ""
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
}
binding.backButtonHitArea.onClickNavBack()
R.id.action_nav_send_memo_to_nav_send_address.let {
binding.backButtonHitArea.onClickNavTo(it)
onBackPressNavTo(it)
}
binding.radioIncludeAddress.setOnClickListener {
if (binding.radioIncludeAddress.isActivated) {
binding.radioIncludeAddress.isChecked = false

View File

@ -47,6 +47,9 @@ class SendViewModel @Inject constructor() : ViewModel() {
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> {
emit("Please enter a larger amount")
}
synchronizer.getAddress() == toAddress -> {
emit("That appears to be your address!")
}
else -> emit(null)
}
}

View File

@ -32,7 +32,7 @@ import kotlinx.coroutines.withContext
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var hasBackUp: Boolean? = null
private var hasBackUp: Boolean = true //TODO: implement backup and then check for it here-ish
override fun inflate(inflater: LayoutInflater): FragmentBackupBinding =
FragmentBackupBinding.inflate(inflater)
@ -80,7 +80,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
if (showMessage) {
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()
}
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
mainActivity?.navController?.popBackStack()
}
private fun applySpan(vararg textViews: TextView) = lifecycleScope.launch {

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="100%" android:toYDelta="0%"
android:duration="300" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:fromXDelta="100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:interpolator="@android:interpolator/decelerate_cubic"
android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300"/>
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<translate
android:interpolator="@android:interpolator/decelerate_cubic"
android:fromXDelta="0%" android:toXDelta="100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="300"/>
</set>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<solid android:color="@color/text_light_dimmed" />
</shape>
</item>
<item
android:end="1dp"
android:start="1dp"
android:top="1dp">
<shape>
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<stroke
android:color="#282828"
android:width="1dp" />
<solid android:color="@color/background_banner" />
</shape>
</item></layer-list>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient android:startColor="@color/colorPrimary" android:endColor="@color/colorPrimaryDark" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:centerColor="@color/zcashGreen"
android:endColor="@color/zcashBlue"
android:startColor="@color/zcashGreen" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/colorPrimaryMedium"
android:startColor="@color/colorPrimary" />
</shape>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="21dp"
android:viewportWidth="17"
android:viewportHeight="21">
<path
android:pathData="M17,3.3876L17,11.6833C17,14.6427 15.3708,17.348 12.75,18.7407L8.5,21L4.25,18.7407C1.6292,17.348 0,14.6427 0,11.6833L0,3.3876L8.5,0L17,3.3876ZM8.0121,10.8519L11.9036,7L14,9.0741L8.0121,15L4,11.0298L6.0964,8.9558L8.0121,10.8519Z"
android:strokeAlpha="0.4079706"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0.4079706"/>
</vector>

View File

@ -2,9 +2,9 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<!-- -->
@ -68,65 +68,111 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/button_backup"
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Backup\nWallet"
android:text="Updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/text_balance_description"
app:layout_constraintEnd_toStartOf="@id/label_balance"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/back_button" />
<TextView
android:id="@+id/label_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="Available"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_balance_available"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_balance_available" />
<TextView
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(enter an amount to send)"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="24dp"
android:paddingTop="24dp"
android:onClick="copyAddress"
android:background="@drawable/background_header"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/back_button_hit_area">
<TextView
android:id="@+id/text_header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Your Wallet History"
android:textColor="@color/text_light"
android:textSize="22dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/label_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Shielded address: "
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="@id/text_header_title"
app:layout_constraintTop_toBottomOf="@+id/text_header_title" />
<TextView
android:id="@+id/text_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="zs1g7cqw...9qmvyzgm"
android:textColor="@color/colorPrimaryMedium"
app:layout_constraintStart_toEndOf="@id/label_address"
app:layout_constraintTop_toBottomOf="@+id/text_header_title" />
<ImageView
android:id="@+id/image_copy"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_content_copy"
app:layout_constraintTop_toTopOf="@id/label_address"
app:layout_constraintBottom_toBottomOf="@id/label_address"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintDimensionRatio="W,1:1"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_transactions"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="12dp"
app:layout_constraintBottom_toTopOf="@+id/guideline_bottom_buttons"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintTop_toBottomOf="@id/button_backup"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/header"
tools:itemCount="15"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_transaction"
tools:orientation="vertical" />
<View
android:id="@+id/text_banner_message"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_banner_large"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/guideline_bottom_buttons"
app:layout_constraintBottom_toBottomOf="@id/guideline_keyline_bottom"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_feedback"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.White"
android:text="Send Feedback"
android:padding="12dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintVertical_bias="0.8"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_logs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:text="View Logs"
android:textColor="@color/text_light"
app:layout_constraintTop_toBottomOf="@id/button_feedback"
app:layout_constraintBottom_toBottomOf="@id/guideline_keyline_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.2"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -74,7 +74,9 @@
android:layout_height="wrap_content"
android:text="(enter an amount to send)"
android:visibility="gone"
tools:visibility="visible"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
@ -291,14 +293,14 @@
android:layout_height="0dp"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.03825136612"
app:layout_constraintHorizontal_bias="0.8883"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.8883"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.06420765027"
app:layout_constraintWidth_percent="0.05582524272"
app:srcCompat="@drawable/ic_receive_funds" />
app:layout_constraintVertical_bias="0.064"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_account_circle" />
<ImageView
android:id="@+id/icon_detail"
@ -359,7 +361,7 @@
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="$0"
tools:text="$0"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="72dp"
android:maxLines="1"

View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.13" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.15" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />
<ImageView
android:id="@+id/icon_close"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
android:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.8883"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
<ImageView
android:id="@+id/icon_profile"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
android:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.4"
app:srcCompat="@drawable/ic_account_circle" />
<View
android:id="@+id/hit_area_close"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginEnd="24dp"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Body1"
android:textSize="20dp"
android:text="Shielded User"
app:layout_constraintTop_toBottomOf="@id/icon_profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<TextView
android:id="@+id/text_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Zcash.TextAppearance.AddressPart"
android:textSize="16dp"
android:text="zs1g7cqw...9qmvyzgm"
android:textColor="@color/text_light_dimmed"
app:layout_constraintTop_toBottomOf="@id/text_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<ImageView
android:id="@+id/image_copy"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_content_copy"
app:layout_constraintTop_toTopOf="@id/text_address"
app:layout_constraintBottom_toBottomOf="@id/text_address"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintDimensionRatio="W,1:1"/>
<View
android:id="@+id/hit_area_address"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:onClick="copyAddress"
app:layout_constraintStart_toStartOf="@id/text_address"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintTop_toTopOf="@id/text_username"
app:layout_constraintBottom_toTopOf="@id/button_feedback"
tools:background="@color/spacer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_feedback"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button"
android:text="Send Feedback"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
android:gravity="center"
android:padding="12dp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintTop_toBottomOf="@id/text_address"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_backup"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:text="Backup Wallet"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:gravity="center"
android:padding="12dp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintTop_toBottomOf="@id/button_feedback" />
<TextView
android:id="@+id/button_logs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/TextAppearance.AppCompat.Body1"
android:textSize="16sp"
android:text="See Application Log"
app:layout_constraintTop_toBottomOf="@id/button_backup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- -->
<!-- Banner -->
<!-- -->
<TextView
android:id="@+id/text_banner_message"
android:elevation="6dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_banner"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:text="zECC App"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_logs"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/text_version"
android:elevation="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="v1.0.0-alpha05"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -88,7 +88,6 @@
android:id="@+id/icon_qr_logo"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:onClick="copyAddress"
android:scaleType="fitCenter"
android:src="@drawable/ic_zcash_primary"
@ -135,6 +134,27 @@
app:layout_constraintStart_toEndOf="@id/receive_address"
app:layout_constraintTop_toTopOf="@id/receive_address" />
<Space
android:id="@+id/space_address_median"
android:layout_width="1dp"
android:layout_height="1dp"
app:layout_constraintTop_toBottomOf="@id/receive_address"
app:layout_constraintBottom_toTopOf="@id/button_scan"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.4"/>
<View
android:id="@+id/hit_area_address"
android:layout_width="0dp"
android:layout_height="0dp"
android:onClick="copyAddress"
app:layout_constraintStart_toStartOf="@id/receive_address"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintTop_toTopOf="@id/receive_title"
app:layout_constraintBottom_toTopOf="@id/space_address_median"
tools:background="@color/spacer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_scan"
android:layout_width="0dp"

View File

@ -91,17 +91,19 @@
<TextView
android:id="@+id/text_address_error"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/inconsolata"
android:includeFontPadding="false"
android:textColor="@android:color/holo_red_light"
android:maxLines="1"
android:autoSizeTextType="uniform"
android:textSize="14dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/button_next"
app:layout_constraintStart_toStartOf="@+id/input_zcash_amount"
app:layout_constraintTop_toTopOf="@+id/button_next"
tools:text="Please enter a larger amount of money" />
app:layout_constraintEnd_toStartOf="@id/button_next"
tools:text="Please enter a larger amount of money also please enter a shorter sentence" />
<!-- Scan QR code -->
<ImageView

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_transaction"
@ -7,58 +8,125 @@
android:layout_height="wrap_content"
android:background="@color/background_banner"
android:elevation="1dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingEnd="10dp"
android:paddingStart="8dp"
android:layout_marginTop="4dp"
tools:ignore="RtlSymmetry">
<!-- -->
<!-- Borders -->
<!-- -->
<View
android:id="@+id/divider_top"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider_left"
android:layout_width="1dp"
android:layout_height="0dp"
android:background="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<View
android:id="@+id/divider_right"
android:layout_width="1dp"
android:layout_height="0dp"
android:background="@color/text_light_dimmed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<View
android:id="@+id/indicator"
android:layout_width="4dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="3dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:background="@drawable/background_indicator_inbound"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@color/zcashGreen"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_transaction_top"
<ImageView
android:id="@+id/image_shield"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:src="@drawable/ic_check_shield"
app:layout_constraintBottom_toTopOf="@id/text_transaction_bottom"
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/image_shield_invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@drawable/ic_check_shield"
android:tint="@color/zcashRed"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/text_transaction_bottom"
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toTopOf="@id/indicator"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_transaction_top"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/text_light"
android:textSize="14sp"
android:maxLines="1"
android:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/image_shield_invisible"
app:layout_constraintStart_toEndOf="@id/image_shield"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintTop_toTopOf="@id/image_shield_invisible"
app:layout_constraintVertical_chainStyle="packed"
tools:text="8/23 3:24pm" />
app:layout_goneMarginStart="16dp"
tools:text="This is a very long sentence and it better not messup the UI" />
<TextView
android:id="@+id/text_transaction_bottom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:paddingEnd="16dp"
android:maxLines="1"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingEnd="16dp"
android:autoSizeTextType="uniform"
android:text="This is a very long sentence and it better not messup the UI"
android:textColor="@color/text_light_dimmed"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toStartOf="@id/text_transaction_top"
app:layout_constraintTop_toBottomOf="@id/text_transaction_top"
app:layout_constraintBottom_toBottomOf="parent"
android:text="funds received" />
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toBottomOf="@id/image_shield_invisible" />
<TextView
android:id="@+id/text_transaction_amount"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="+ 4.244"
android:layout_marginEnd="16dp"
android:autoSizeTextType="uniform"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@id/indicator"
app:layout_constraintWidth_percent="0.25"
tools:text="+ 4345.2444" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,16 +15,22 @@
app:destination="@id/nav_receive" />
<action
android:id="@+id/action_nav_home_to_nav_detail"
app:destination="@id/nav_detail" />
app:destination="@id/nav_detail"
app:enterAnim="@anim/anim_enter_from_bottom" />
<action
android:id="@+id/action_nav_home_to_create_wallet"
app:destination="@id/nav_landing" />
<action
android:id="@+id/action_nav_home_to_send"
app:destination="@id/nav_send_address" />
app:destination="@id/nav_send_address"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_home_to_nav_scan"
app:destination="@id/nav_scan" />
<action
android:id="@+id/action_nav_home_to_nav_profile"
app:destination="@id/nav_profile" />
</fragment>
<fragment
@ -57,13 +63,17 @@
<fragment
android:id="@+id/nav_detail"
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
tools:layout="@layout/fragment_detail" >
tools:layout="@layout/fragment_detail" />
<fragment
android:id="@+id/nav_profile"
android:name="cash.z.ecc.android.ui.profile.ProfileFragment"
tools:layout="@layout/fragment_profile" >
<action
android:id="@+id/action_nav_detail_to_backup_wallet"
android:id="@+id/action_nav_profile_to_nav_backup"
app:destination="@id/nav_backup" />
</fragment>
<!-- -->
<!-- Send Navigation -->
<!-- -->
@ -74,10 +84,17 @@
tools:layout="@layout/fragment_send_address" >
<action
android:id="@+id/action_nav_send_address_to_send_memo"
app:destination="@id/nav_send_memo"/>
app:destination="@id/nav_send_memo"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_send_address_to_nav_scan"
app:destination="@id/nav_scan" />
<action
android:id="@+id/action_nav_send_address_to_nav_home"
app:destination="@id/nav_home"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right"/>
</fragment>
<fragment
@ -86,7 +103,14 @@
tools:layout="@layout/fragment_send_memo" >
<action
android:id="@+id/action_nav_send_memo_to_send_confirm"
app:destination="@id/nav_send_confirm"/>
app:destination="@id/nav_send_confirm"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_send_memo_to_nav_send_address"
app:destination="@id/nav_send_address"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right" />
</fragment>
<fragment
@ -95,7 +119,15 @@
tools:layout="@layout/fragment_send_confirm" >
<action
android:id="@+id/action_nav_send_confirm_to_send_final"
app:destination="@id/nav_send_final"/>
app:destination="@id/nav_send_final"
app:popUpTo="@id/nav_send_confirm"
app:popUpToInclusive="true"
app:enterAnim="@anim/anim_fade_in"/>
<action
android:id="@+id/action_nav_send_confirm_to_nav_send_memo"
app:destination="@id/nav_send_memo"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right" />
</fragment>
<fragment

View File

@ -8,6 +8,7 @@
<!-- colors here should map to a material design value -->
<color name="colorPrimary">@color/zcashYellow</color>
<color name="colorPrimaryMedium">#805E08</color>
<color name="colorPrimaryDark">#4D3805</color>
<color name="colorAccent">#A1A1A1</color>
<color name="colorSurface">@color/text_light</color>
@ -40,6 +41,7 @@
<color name="zcashGreen">#66BB6A</color>
<color name="zcashRed">#BB666A</color>
<color name="zcashBlue">#26DAB6</color>
<!-- yellows -->
<color name="zcashYellow_light">#FFD649</color>
@ -56,6 +58,7 @@
<color name="background_banner">@color/zcashBlack_dark</color>
<color name="scan_overlay_background">@color/zcashBlack_87</color>
<color name="spacer">#1FBB666A</color>
<!-- text -->
<color name="text_light">#FFFFFF</color>

View File

@ -5,6 +5,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.coroutines.coroutineContext
class Feedback(capacity: Int = 256) {
@ -106,10 +108,11 @@ class Feedback(capacity: Int = 256) {
*
* @param metric the metric to add.
*/
fun report(metric: Metric) {
fun report(metric: Metric): Feedback {
jobs += scope.launch {
_metrics.send(metric)
}
return this
}
/**
@ -117,10 +120,21 @@ class Feedback(capacity: Int = 256) {
*
* @param action the action to add.
*/
fun report(action: Action) {
fun report(action: Action): Feedback {
jobs += scope.launch {
_actions.send(action)
}
return this
}
/**
* Report the given error to everything that is tracking feedback. Converts it to a Crash object
* which is intended for use in property-based analytics.
*
* @param error the uncaught exception that occurred.
*/
fun report(error: Throwable?): Feedback {
return report(Crash(error))
}
/**
@ -147,6 +161,7 @@ class Feedback(capacity: Int = 256) {
throw IllegalStateException("Feedback is still active because ${errors.joinToString(", ")}.")
}
interface Metric : Mappable<String, Any> {
val key: String
val startTime: Long?
@ -193,4 +208,30 @@ class Feedback(capacity: Int = 256) {
return "$description in ${elapsedTime}ms"
}
}
}
data class Crash(val error: Throwable?) : Action {
override val key: String = "crash"
override fun toMap(): Map<String, Any> {
return mutableMapOf<String, Any>(
"message" to (error?.message ?: "None"),
"cause" to (error?.cause?.toString() ?: "None"),
"cause.cause" to (error?.cause?.cause?.toString() ?: "None"),
"cause.cause.cause" to (error?.cause?.cause?.cause?.toString() ?: "None")
).apply { putAll(super.toMap()); putAll(error.stacktraceToMap()) }
}
override fun toString() = "App crashed due to: $error"
}
}
private fun Throwable?.stacktraceToMap(chunkSize: Int = 250): Map<out String, String> {
val properties = mutableMapOf("stacktrace0" to "None")
if (this == null) return properties
val stringWriter = StringWriter()
printStackTrace(PrintWriter(stringWriter))
stringWriter.toString().chunked(chunkSize).forEachIndexed { index, chunk ->
properties["stacktrace$index"] = chunk
}
return properties
}