commit
5cdfc97945
|
@ -8,7 +8,7 @@ apply plugin: 'kotlin-kapt'
|
|||
|
||||
archivesBaseName = 'zcash-android-wallet'
|
||||
group = 'cash.z.ecc.android'
|
||||
version = '1.0.0-alpha03'
|
||||
version = '1.0.0-alpha04'
|
||||
|
||||
android {
|
||||
compileSdkVersion Deps.compileSdkVersion
|
||||
|
@ -18,10 +18,12 @@ android {
|
|||
applicationId 'cash.z.ecc.android'
|
||||
minSdkVersion Deps.minSdkVersion
|
||||
targetSdkVersion Deps.targetSdkVersion
|
||||
versionCode = 1_00_00_003
|
||||
versionCode = 1_00_00_004
|
||||
// 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'
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
multiDexEnabled true
|
||||
}
|
||||
flavorDimensions 'network'
|
||||
productFlavors {
|
||||
|
@ -40,11 +42,13 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
useProguard false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled true
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
useProguard false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
@ -82,6 +86,7 @@ dependencies {
|
|||
implementation project(':feedback')
|
||||
implementation project(':mnemonic')
|
||||
implementation project(':lockbox')
|
||||
implementation project(':sdk')
|
||||
|
||||
// Kotlin
|
||||
implementation Deps.Kotlin.STDLIB
|
||||
|
@ -102,6 +107,11 @@ dependencies {
|
|||
kapt Deps.Dagger.ANDROID_PROCESSOR
|
||||
kapt Deps.Dagger.COMPILER
|
||||
|
||||
// Testing these BIP39 dependencies
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'io.github.novacrypto:BIP39:2019.01.27'
|
||||
implementation 'io.github.novacrypto:securestring:2019.01.27'
|
||||
|
||||
// grpc-java
|
||||
implementation "io.grpc:grpc-okhttp:1.21.0"
|
||||
implementation "io.grpc:grpc-android:1.21.0"
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Reports
|
||||
-printusage build/outputs/logs/R8-removed-code-report.txt
|
||||
-printseeds build/outputs/logs/R8-entry-points-report.txt
|
||||
|
||||
## Okio
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
#-keep class cash.z.** { *; }
|
|
@ -3,16 +3,116 @@ package cash.z.ecc.android.integration
|
|||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import okio.Buffer
|
||||
import okio.GzipSink
|
||||
import okio.Okio
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class IntegrationTest {
|
||||
|
||||
private lateinit var appContext: Context
|
||||
private val mnemonics = Mnemonics()
|
||||
private val phrase =
|
||||
"human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor" +
|
||||
" under absorb spirit hurdle animal original honey owner upper empower describe"
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_generation() {
|
||||
val seed = mnemonics.toSeed(phrase.toCharArray())
|
||||
assertEquals(
|
||||
"Generated incorrect BIP-39 seed!",
|
||||
"f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" +
|
||||
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
|
||||
seed.toHex()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_storage() {
|
||||
val seed = mnemonics.toSeed(phrase.toCharArray())
|
||||
val lb = LockBox(appContext)
|
||||
lb.setBytes("seed", seed)
|
||||
assertTrue(seed.contentEquals(lb.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPhrase_storage() {
|
||||
val lb = LockBox(appContext)
|
||||
val phraseChars = phrase.toCharArray()
|
||||
lb.setCharsUtf8("phrase", phraseChars)
|
||||
assertTrue(phraseChars.contentEquals(lb.getCharsUtf8("phrase")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPhrase_maxLengthStorage() {
|
||||
val lb = LockBox(appContext)
|
||||
// find and expose the max length
|
||||
var acceptedSize = 256
|
||||
while (acceptedSize > 0) {
|
||||
try {
|
||||
lb.setCharsUtf8("temp", nextString(acceptedSize).toCharArray())
|
||||
break
|
||||
} catch (t: Throwable) {
|
||||
}
|
||||
acceptedSize--
|
||||
}
|
||||
|
||||
val maxSeedPhraseLength = 8 * 24 + 23 //215 (max length of each word is 8)
|
||||
assertTrue(
|
||||
"LockBox does not support the maximum length seed phrase." +
|
||||
" Expected: $maxSeedPhraseLength but was: $acceptedSize",
|
||||
acceptedSize > maxSeedPhraseLength
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddress() {
|
||||
val seed = mnemonics.toSeed(phrase.toCharArray())
|
||||
val initializer = Initializer(appContext).apply {
|
||||
new(seed, overwrite = true)
|
||||
}
|
||||
assertEquals(
|
||||
"Generated incorrect z-address!",
|
||||
"zs1gn2ah0zqhsxnrqwuvwmgxpl5h3ha033qexhsz8tems53fw877f4gug353eefd6z8z3n4zxty65c",
|
||||
initializer.rustBackend.getAddress()
|
||||
)
|
||||
initializer.clear()
|
||||
}
|
||||
|
||||
|
||||
private fun ByteArray.toHex(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (b in this)
|
||||
sb.append(String.format("%02x", b))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun String.gzip(): ByteArray {
|
||||
val result = Buffer()
|
||||
val sink = Okio.buffer(GzipSink(result))
|
||||
sink.use {
|
||||
sink.write(toByteArray())
|
||||
}
|
||||
return result.readByteArray()
|
||||
}
|
||||
|
||||
fun nextString(length: Int): String {
|
||||
val allowedChars = "ACGT"
|
||||
return (1..length)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
}
|
||||
}
|
|
@ -4,6 +4,9 @@ import android.content.Context
|
|||
import android.os.Build
|
||||
import cash.z.ecc.android.di.DaggerAppComponent
|
||||
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 dagger.android.AndroidInjector
|
||||
import dagger.android.DaggerApplication
|
||||
import javax.inject.Inject
|
||||
|
@ -23,7 +26,7 @@ class ZcashWalletApp : DaggerApplication() {
|
|||
super.onCreate()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
|
||||
// Twig.plant(TroubleshootingTwig())
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,6 +49,7 @@ class ZcashWalletApp : DaggerApplication() {
|
|||
override fun uncaughtException(t: Thread?, e: Throwable?) {
|
||||
// trackCrash(e, "Top-level exception wasn't caught by anything else!")
|
||||
// Analytics.clear()
|
||||
twig("Uncaught Exception: $e")
|
||||
ogHandler.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import cash.z.ecc.android.ui.MainActivityModule
|
|||
import cash.z.ecc.android.ui.detail.WalletDetailFragmentModule
|
||||
import cash.z.ecc.android.ui.home.HomeFragmentModule
|
||||
import cash.z.ecc.android.ui.receive.ReceiveFragmentModule
|
||||
import cash.z.ecc.android.ui.send.SendFragmentModule
|
||||
import cash.z.ecc.android.ui.send.*
|
||||
import cash.z.ecc.android.ui.setup.BackupFragmentModule
|
||||
import cash.z.ecc.android.ui.setup.LandingFragmentModule
|
||||
import dagger.BindsInstance
|
||||
|
@ -27,7 +27,10 @@ import javax.inject.Singleton
|
|||
// Fragments
|
||||
HomeFragmentModule::class,
|
||||
ReceiveFragmentModule::class,
|
||||
SendFragmentModule::class,
|
||||
SendAddressFragmentModule::class,
|
||||
SendMemoFragmentModule::class,
|
||||
SendConfirmFragmentModule::class,
|
||||
SendFinalFragmentModule::class,
|
||||
WalletDetailFragmentModule::class,
|
||||
LandingFragmentModule::class,
|
||||
BackupFragmentModule::class
|
||||
|
|
|
@ -3,6 +3,8 @@ package cash.z.ecc.android.di
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import cash.z.ecc.android.di.annotation.ViewModelKey
|
||||
import cash.z.ecc.android.ui.home.HomeViewModel
|
||||
import cash.z.ecc.android.ui.send.SendViewModel
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
@ -21,6 +23,16 @@ abstract class ViewModelModule {
|
|||
@IntoMap
|
||||
@ViewModelKey(WalletSetupViewModel::class)
|
||||
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(HomeViewModel::class)
|
||||
abstract fun bindHomeViewModel(implementation: HomeViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SendViewModel::class)
|
||||
abstract fun bindSendViewModel(implementation: SendViewModel): ViewModel
|
||||
}
|
||||
|
||||
@Singleton
|
||||
|
|
|
@ -14,6 +14,10 @@ fun View.invisibleIf(isInvisible: Boolean) {
|
|||
visibility = if (isInvisible) INVISIBLE else VISIBLE
|
||||
}
|
||||
|
||||
fun View.disabledIf(isDisabled: Boolean) {
|
||||
isEnabled = !isDisabled
|
||||
}
|
||||
|
||||
fun View.onClickNavTo(navResId: Int) {
|
||||
setOnClickListener {
|
||||
(context as? MainActivity)?.navController?.navigate(navResId)
|
||||
|
@ -32,6 +36,16 @@ fun View.onClickNavUp() {
|
|||
}
|
||||
}
|
||||
|
||||
fun View.onClickNavBack() {
|
||||
setOnClickListener {
|
||||
(context as? MainActivity)?.navController?.popBackStack()
|
||||
?: throw IllegalStateException(
|
||||
"Cannot navigate from this activity. " +
|
||||
"Expected MainActivity but found ${context.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.clicks() = channelFlow<View> {
|
||||
setOnClickListener {
|
||||
offer(this@clicks)
|
||||
|
|
|
@ -2,15 +2,25 @@ package cash.z.ecc.android.feedback
|
|||
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
|
||||
object Report {
|
||||
enum class NonUserAction(override val key: String, val description: String) : Feedback.Action {
|
||||
FEEDBACK_STARTED("action.feedback.start", "feedback started"),
|
||||
FEEDBACK_STOPPED("action.feedback.stop", "feedback stopped");
|
||||
FEEDBACK_STOPPED("action.feedback.stop", "feedback stopped"),
|
||||
SYNC_START("action.feedback.synchronizer.start", "sync started");
|
||||
|
||||
override fun toString(): String = description
|
||||
}
|
||||
|
||||
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
|
||||
SEED_CREATION("metric.seed.creation", "seed created")
|
||||
ENTROPY_CREATED("metric.entropy.created", "entropy created"),
|
||||
SEED_CREATED("metric.seed.created", "seed created"),
|
||||
SEED_IMPORTED("metric.seed.imported", "seed imported"),
|
||||
SEED_PHRASE_CREATED("metric.seedphrase.created", "seed phrase created"),
|
||||
SEED_PHRASE_LOADED("metric.seedphrase.loaded", "seed phrase loaded"),
|
||||
WALLET_CREATED("metric.wallet.created", "wallet created"),
|
||||
WALLET_IMPORTED("metric.wallet.imported", "wallet imported"),
|
||||
ACCOUNT_CREATED("metric.account.created", "account created")
|
||||
}
|
||||
}
|
||||
|
||||
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
|
||||
|
@ -27,5 +37,5 @@ class LaunchMetric private constructor(private val metric: Feedback.TimeMetric)
|
|||
override fun toString(): String = metric.toString()
|
||||
}
|
||||
|
||||
fun <T> Feedback.measure(type: MetricType, block: () -> T) =
|
||||
inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T =
|
||||
this.measure(type.key, type.description, block)
|
|
@ -13,7 +13,9 @@ import android.view.ViewGroup
|
|||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
|
@ -21,6 +23,12 @@ import cash.z.ecc.android.R
|
|||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.di.annotation.ActivityScope
|
||||
import cash.z.ecc.android.feedback.*
|
||||
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
|
||||
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
|
||||
import cash.z.ecc.android.ui.send.SendViewModel
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -33,18 +41,29 @@ import javax.inject.Inject
|
|||
|
||||
class MainActivity : DaggerAppCompatActivity() {
|
||||
|
||||
private var syncInit: (() -> Unit)? = null
|
||||
|
||||
@Inject
|
||||
lateinit var feedback: Feedback
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
val sendViewModel: SendViewModel by viewModels { viewModelFactory }
|
||||
|
||||
lateinit var navController: NavController
|
||||
|
||||
private val mediaPlayer: MediaPlayer = MediaPlayer()
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
|
||||
lateinit var synchronizer: Synchronizer
|
||||
|
||||
val clipboard get() = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
|
@ -78,7 +97,7 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
lifecycleScope.launch {
|
||||
feedback.report(NonUserAction.FEEDBACK_STOPPED)
|
||||
feedback.report(FEEDBACK_STOPPED)
|
||||
feedback.stop()
|
||||
}
|
||||
super.onDestroy()
|
||||
|
@ -106,6 +125,29 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
fun initSync() {
|
||||
twig("Initializing synchronizer")
|
||||
if (!::synchronizer.isInitialized) {
|
||||
twig("Synchronizer didn't exist yet (this means we're opening an existing wallet). Creating it now.")
|
||||
val initializer = Initializer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443).also { it.open() }
|
||||
synchronizer = Synchronizer(ZcashWalletApp.instance, initializer)
|
||||
}
|
||||
feedback.report(SYNC_START)
|
||||
synchronizer.start(lifecycleScope)
|
||||
if (syncInit != null) {
|
||||
syncInit!!()
|
||||
syncInit = null
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeAccount(seed: ByteArray, birthday: Initializer.WalletBirthday? = null) {
|
||||
twig("Initializing accounts")
|
||||
feedback.measure(Report.MetricType.ACCOUNT_CREATED) {
|
||||
synchronizer =
|
||||
Synchronizer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443, seed, birthday)
|
||||
}
|
||||
}
|
||||
|
||||
fun playSound(fileName: String) {
|
||||
mediaPlayer.apply {
|
||||
if (isPlaying) stop()
|
||||
|
@ -131,19 +173,16 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
}
|
||||
|
||||
fun copyAddress(view: View) {
|
||||
// TODO: get address from synchronizer
|
||||
val address =
|
||||
"zs1qduvdyuv83pyygjvc4cfcuc2wj5flnqn730iigf0tjct8k5ccs9y30p96j2gvn9gzyxm6q0vj12c4"
|
||||
val clipboard: ClipboardManager =
|
||||
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
lifecycleScope.launch {
|
||||
clipboard.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
"Z-Address",
|
||||
address
|
||||
synchronizer.getAddress()
|
||||
)
|
||||
)
|
||||
showMessage("Address copied!", "Sweet")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMessage(message: String, action: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
|
@ -174,6 +213,15 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
if (!it.isShownOrQueued) it.show()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor initialization and remove the need for this
|
||||
fun onSyncInit(initBlock: () -> Unit) {
|
||||
if (::synchronizer.isInitialized) {
|
||||
initBlock()
|
||||
} else {
|
||||
syncInit = initBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -5,15 +5,23 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cash.z.ecc.android.ui.MainActivity
|
||||
import dagger.android.support.DaggerFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
abstract class BaseFragment<T : ViewBinding> : DaggerFragment() {
|
||||
val mainActivity: MainActivity? get() = activity as MainActivity?
|
||||
|
||||
lateinit var binding: T
|
||||
|
||||
lateinit var resumedScope: CoroutineScope
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -23,6 +31,18 @@ abstract class BaseFragment<T : ViewBinding> : DaggerFragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumedScope = lifecycleScope.coroutineContext.let {
|
||||
CoroutineScope(it + SupervisorJob(it[Job]))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
resumedScope.cancel()
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -40,6 +40,8 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
|
|||
mainActivity?.showSnackbar("Feedback not yet implemented.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onViewLogs() {
|
||||
loadLogFileAsText().let { logText ->
|
||||
if (logText == null) {
|
||||
|
|
|
@ -1,89 +1,255 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentHomeBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
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.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
|
||||
import cash.z.wallet.sdk.SdkSynchronizer
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.SYNCING
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
|
||||
private lateinit var numberPad: List<TextView>
|
||||
private lateinit var uiModel: HomeViewModel.UiModel
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
|
||||
private val viewModel: HomeViewModel by activityViewModels { viewModelFactory }
|
||||
|
||||
private val _typedChars = ConflatedBroadcastChannel<Char>()
|
||||
private val typedChars = _typedChars.asFlow()
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
|
||||
FragmentHomeBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
numberPad = arrayListOf(
|
||||
buttonNumberPad0,
|
||||
buttonNumberPad1,
|
||||
buttonNumberPad2,
|
||||
buttonNumberPad3,
|
||||
buttonNumberPad4,
|
||||
buttonNumberPad5,
|
||||
buttonNumberPad6,
|
||||
buttonNumberPad7,
|
||||
buttonNumberPad8,
|
||||
buttonNumberPad9,
|
||||
buttonNumberPadDecimal,
|
||||
buttonNumberPadBack
|
||||
)
|
||||
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive)
|
||||
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_send)
|
||||
|
||||
textBannerAction.setOnClickListener {
|
||||
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: trigger this from presenter
|
||||
onNoFunds()
|
||||
}
|
||||
//
|
||||
// LifeCycle
|
||||
//
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
twig("HomeFragment.onAttach")
|
||||
super.onAttach(context)
|
||||
|
||||
// call initSync either now or later (after initializing DBs with newly created seed)
|
||||
walletSetup.checkSeed().onEach {
|
||||
twig("Checking seed")
|
||||
when(it) {
|
||||
NO_SEED -> {
|
||||
twig("Seed not found, therefore, launching seed creation flow")
|
||||
// interact with user to create, backup and verify seed
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
|
||||
// leads to a call to initSync(), later (after accounts are created from seed)
|
||||
}
|
||||
else -> {
|
||||
twig("Found seed. Re-opening existing wallet")
|
||||
mainActivity?.initSync()
|
||||
}
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
twig("HomeFragment.onViewCreated uiModel: ${::uiModel.isInitialized} saved: ${savedInstanceState != null}")
|
||||
with(binding) {
|
||||
numberPad = arrayListOf(
|
||||
buttonNumberPad0.asKey(),
|
||||
buttonNumberPad1.asKey(),
|
||||
buttonNumberPad2.asKey(),
|
||||
buttonNumberPad3.asKey(),
|
||||
buttonNumberPad4.asKey(),
|
||||
buttonNumberPad5.asKey(),
|
||||
buttonNumberPad6.asKey(),
|
||||
buttonNumberPad7.asKey(),
|
||||
buttonNumberPad8.asKey(),
|
||||
buttonNumberPad9.asKey(),
|
||||
buttonNumberPadDecimal.asKey(),
|
||||
buttonNumberPadBack.asKey()
|
||||
)
|
||||
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive)
|
||||
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_send)
|
||||
|
||||
textBannerAction.setOnClickListener {
|
||||
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
|
||||
}
|
||||
buttonSend.setOnClickListener {
|
||||
onSend()
|
||||
}
|
||||
}
|
||||
if (::uiModel.isInitialized) {
|
||||
twig("uiModel exists!")
|
||||
onModelUpdated(HomeViewModel.UiModel(), uiModel)
|
||||
} else {
|
||||
twig("uiModel does not exist!")
|
||||
mainActivity?.onSyncInit {
|
||||
viewModel.initialize(mainActivity!!.synchronizer, typedChars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
|
||||
viewModel.uiModels.scanReduce { old, new ->
|
||||
onModelUpdated(old, new)
|
||||
new
|
||||
}.catch { e ->
|
||||
twig("exception while processing uiModels $e")
|
||||
}.launchIn(resumedScope)
|
||||
|
||||
// TODO: see if there is a better way to trigger a refresh of the uiModel on resume
|
||||
// the latest one should just be in the viewmodel and we should just "resubscribe"
|
||||
// but for some reason, this doesn't always happen, which kind of defeats the purpose
|
||||
// of having a cold stream in the view model
|
||||
resumedScope.launch {
|
||||
(mainActivity!!.synchronizer as SdkSynchronizer).refreshBalance()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
twig("HomeFragment.onSaveInstanceState")
|
||||
if (::uiModel.isInitialized) {
|
||||
outState.putParcelable("uiModel", uiModel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
savedInstanceState?.let { inState ->
|
||||
twig("HomeFragment.onViewStateRestored")
|
||||
onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Public UI API
|
||||
//
|
||||
|
||||
fun setSendEnabled(enabled: Boolean) {
|
||||
binding.buttonSend.apply {
|
||||
isEnabled = enabled
|
||||
backgroundTintList = ColorStateList.valueOf( resources.getColor( if(enabled) R.color.colorPrimary else R.color.zcashWhite_24) )
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgress(progress: Int) {
|
||||
progress.let {
|
||||
if (it < 100) {
|
||||
setBanner("Downloading . . . $it%", NONE)
|
||||
} else {
|
||||
setBanner("Scanning . . .", NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSendAmount(amount: String) {
|
||||
binding.textSendAmount.text = "\$$amount"
|
||||
mainActivity?.sendViewModel?.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
|
||||
binding.buttonSend.disabledIf(amount == "0")
|
||||
}
|
||||
|
||||
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) {
|
||||
val availableString = if (availableBalance < 0) "Updating" else availableBalance.convertZatoshiToZecString()
|
||||
binding.textBalanceAvailable.text = availableString
|
||||
binding.textBalanceDescription.apply {
|
||||
goneIf(availableBalance < 0)
|
||||
text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
|
||||
"(expecting +${(totalBalance - availableBalance).convertZatoshiToZecString()} ZEC in change)"
|
||||
} else {
|
||||
"(enter an amount to send)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSendText(buttonText: String = "Send Amount") {
|
||||
binding.buttonSend.text = buttonText
|
||||
}
|
||||
|
||||
fun setBanner(message: String = "", action: BannerAction = CLEAR) {
|
||||
with(binding) {
|
||||
val hasMessage = !message.isEmpty() || action != CLEAR
|
||||
groupBalance.goneIf(hasMessage)
|
||||
groupBanner.goneIf(!hasMessage)
|
||||
layerLock.goneIf(!hasMessage)
|
||||
|
||||
textBannerMessage.text = message
|
||||
textBannerAction.text = action.action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Private UI Events
|
||||
//
|
||||
|
||||
private fun onModelUpdated(old: HomeViewModel.UiModel, new: HomeViewModel.UiModel) {
|
||||
twig(new.toString())
|
||||
uiModel = new
|
||||
if (old.pendingSend != new.pendingSend) {
|
||||
setSendAmount(new.pendingSend)
|
||||
}
|
||||
// TODO: handle stopped and disconnected flows
|
||||
if (new.status == SYNCING) onSyncing(new) else onSynced(new)
|
||||
setSendEnabled(new.isSendEnabled)
|
||||
}
|
||||
|
||||
private fun onSyncing(uiModel: HomeViewModel.UiModel) {
|
||||
setProgress(uiModel.progress) // calls setBanner
|
||||
setAvailable()
|
||||
setSendText("Syncing Blockchain…")
|
||||
}
|
||||
|
||||
private fun onSynced(uiModel: HomeViewModel.UiModel) {
|
||||
if (!uiModel.hasFunds) {
|
||||
onNoFunds()
|
||||
} else {
|
||||
setBanner("")
|
||||
setAvailable(uiModel.availableBalance, uiModel.totalBalance)
|
||||
setSendText()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSend() {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_home_to_send)
|
||||
}
|
||||
|
||||
private fun onBannerAction(action: BannerAction) {
|
||||
when (action) {
|
||||
LEARN_MORE -> {
|
||||
FUND_NOW -> {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setMessage("To make full use of this wallet, deposit funds to your address or tap the faucet to trigger a tiny automatic deposit.\n\nFaucet funds are made available for the community by the community for testing. So please be kind enough to return what you borrow!")
|
||||
.setTitle("No Balance")
|
||||
|
@ -106,25 +272,19 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
|
||||
private fun onNoFunds() {
|
||||
setBanner("No Balance", LEARN_MORE)
|
||||
setBanner("No Balance", FUND_NOW)
|
||||
}
|
||||
|
||||
private fun setBanner(message: String = "", action: BannerAction = CLEAR) {
|
||||
with(binding) {
|
||||
val hasMessage = !message.isEmpty() || action != CLEAR
|
||||
groupBalance.goneIf(hasMessage)
|
||||
groupBanner.goneIf(!hasMessage)
|
||||
layerLock.goneIf(!hasMessage)
|
||||
|
||||
textBannerMessage.text = message
|
||||
textBannerAction.text = action.action
|
||||
}
|
||||
}
|
||||
//
|
||||
// Inner classes and extensions
|
||||
//
|
||||
|
||||
enum class BannerAction(val action: String) {
|
||||
LEARN_MORE("Learn More"),
|
||||
FUND_NOW("Fund Now"),
|
||||
CANCEL("Cancel"),
|
||||
CLEAR("");
|
||||
NONE(""),
|
||||
CLEAR("clear");
|
||||
|
||||
companion object {
|
||||
fun from(action: String?): BannerAction {
|
||||
|
@ -135,6 +295,54 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextView.asKey(): TextView {
|
||||
val c = text[0]
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
twig("CHAR TYPED: $c")
|
||||
_typedChars.send(c)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO: remove these troubleshooting logs
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
twig("HomeFragment.onCreate")
|
||||
}
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
twig("HomeFragment.onActivityCreated")
|
||||
}
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
twig("HomeFragment.onStart")
|
||||
}
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
twig("HomeFragment.onPause resumeScope.isActive: ${resumedScope.isActive}")
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
twig("HomeFragment.onStop")
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
twig("HomeFragment.onDestroyView")
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
twig("HomeFragment.onDestroy")
|
||||
}
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
twig("HomeFragment.onDetach")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.DISCONNECTED
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
lateinit var uiModels: Flow<UiModel>
|
||||
|
||||
fun initialize(
|
||||
synchronizer: Synchronizer,
|
||||
typedChars: Flow<Char>
|
||||
) {
|
||||
twig("init called")
|
||||
val zec = typedChars.scan("0") { acc, c ->
|
||||
when {
|
||||
// no-op cases
|
||||
acc == "0" && c == '0'
|
||||
|| (c == '<' && acc == "0")
|
||||
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c $typedChars ")
|
||||
acc
|
||||
}
|
||||
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
|
||||
"0"
|
||||
}
|
||||
c == '<' -> {twig("triggered: 3")
|
||||
acc.substring(0, acc.length - 1)
|
||||
}
|
||||
acc == "0" && c != '.' -> {twig("triggered: 4 $typedChars")
|
||||
c.toString()
|
||||
}
|
||||
else -> {twig("triggered: 5 $typedChars")
|
||||
"$acc$c"
|
||||
}
|
||||
}
|
||||
}
|
||||
uiModels = synchronizer.run {
|
||||
combine(status, progress, balances, zec) { s, p, b, z->
|
||||
UiModel(s, p, b.available, b.total, z)
|
||||
}
|
||||
}.conflate()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
twig("HomeViewModel cleared!")
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
|
||||
val status: Synchronizer.Status = DISCONNECTED,
|
||||
val progress: Int = 0,
|
||||
val availableBalance: Long = -1L,
|
||||
val totalBalance: Long = -1L,
|
||||
val pendingSend: String = "0"
|
||||
): 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 isSynced: Boolean get() = status == SYNCED
|
||||
val isSendEnabled: Boolean get() = isSynced && hasFunds
|
||||
}
|
||||
}
|
|
@ -16,10 +16,14 @@ import cash.z.ecc.android.ext.onClickNavTo
|
|||
import cash.z.ecc.android.ext.onClickNavUp
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.android.synthetic.main.fragment_receive.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
|
||||
override fun inflate(inflater: LayoutInflater): FragmentReceiveBinding =
|
||||
|
@ -51,23 +55,39 @@ class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleScope.launch {
|
||||
onAddressLoaded("zs1qduvdyuv83pyygjvc4cfcuc2wj5flnqn730iigf0tjct8k5ccs9y30p96j2gvn9gzyxm6q0vj12c4")
|
||||
resumedScope.launch {
|
||||
mainActivity?.synchronizer?.getAddress()?.let { address ->
|
||||
onAddressLoaded(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddressLoaded(address: String) {
|
||||
Log.e("TWIG", "onAddressLoaded: $address length: ${address.length}")
|
||||
twig("address loaded: $address length: ${address.length}")
|
||||
qrecycler.load(address)
|
||||
.withQuietZoneSize(3)
|
||||
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
|
||||
.into(receive_qr_code)
|
||||
|
||||
address.chunked(address.length/8).forEachIndexed { i, part ->
|
||||
address.distribute(8) { i, part ->
|
||||
setAddressPart(i, part)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> String.distribute(chunks: Int, block: (Int, String) -> T) {
|
||||
val charsPerChunk = length / 8.0
|
||||
val wholeCharsPerChunk = charsPerChunk.toInt()
|
||||
val chunksWithExtra = ((charsPerChunk - wholeCharsPerChunk) * chunks).roundToInt()
|
||||
repeat(chunks) { i ->
|
||||
val part = if (i < chunksWithExtra) {
|
||||
substring(i * (wholeCharsPerChunk + 1), (i + 1) * (wholeCharsPerChunk + 1))
|
||||
} else {
|
||||
substring(i * wholeCharsPerChunk + chunksWithExtra, (i + 1) * wholeCharsPerChunk + chunksWithExtra)
|
||||
}
|
||||
block(i, part)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAddressPart(index: Int, addressPart: String) {
|
||||
Log.e("TWIG", "setting address for part $index) $addressPart")
|
||||
val thinSpace = "\u2005" // 0.25 em space
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentSendAddressBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.goneIf
|
||||
import cash.z.ecc.android.ext.onClickNavBack
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
||||
ClipboardManager.OnPrimaryClipChangedListener {
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendAddressBinding =
|
||||
FragmentSendAddressBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onAddAddress()
|
||||
}
|
||||
binding.backButtonHitArea.onClickNavBack()
|
||||
binding.textBannerAction.setOnClickListener {
|
||||
onPaste()
|
||||
}
|
||||
binding.textBannerMessage.setOnClickListener {
|
||||
onPaste()
|
||||
}
|
||||
binding.textAmount.text = "Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC"
|
||||
binding.inputZcashAddress.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onAddAddress()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddAddress() {
|
||||
mainActivity?.sendViewModel?.toAddress = binding.inputZcashAddress.text.toString()
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_address_to_send_memo)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
mainActivity?.clipboard?.removePrimaryClipChangedListener(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateClipboardBanner()
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
twig("clipboard changed!")
|
||||
updateClipboardBanner()
|
||||
}
|
||||
|
||||
private fun updateClipboardBanner() {
|
||||
binding.groupBanner.goneIf(loadAddressFromClipboard() == null)
|
||||
}
|
||||
|
||||
private fun onPaste() {
|
||||
mainActivity?.clipboard?.let { clipboard ->
|
||||
if (clipboard.hasPrimaryClip()) {
|
||||
binding.inputZcashAddress.setText(clipboard.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAddressFromClipboard(): String? {
|
||||
mainActivity?.clipboard?.apply {
|
||||
if (hasPrimaryClip()) {
|
||||
text()?.let { text ->
|
||||
if (text.startsWith("zs") && text.length > 70) {
|
||||
return@loadAddressFromClipboard text.toString()
|
||||
}
|
||||
// treat t-addrs differently in the future
|
||||
if (text.startsWith("t1") && text.length > 32) {
|
||||
return@loadAddressFromClipboard text.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ClipboardManager.text(): CharSequence =
|
||||
primaryClip!!.getItemAt(0).coerceToText(mainActivity)
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class SendAddressFragmentModule {
|
||||
@FragmentScope
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeFragment(): SendAddressFragment
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.goneIf
|
||||
import cash.z.ecc.android.ext.onClickNavBack
|
||||
import cash.z.ecc.android.ui.MainActivity
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.wallet.sdk.ext.abbreviatedAddress
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendConfirmBinding =
|
||||
FragmentSendConfirmBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mainActivity?.apply {
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onSend()
|
||||
}
|
||||
binding.backButtonHitArea.onClickNavBack()
|
||||
mainActivity?.lifecycleScope?.launch {
|
||||
binding.textConfirmation.text =
|
||||
"Send ${sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.abbreviatedAddress()}?"
|
||||
}
|
||||
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
|
||||
binding.radioIncludeAddress.isChecked = hasMemo
|
||||
binding.radioIncludeAddress.goneIf(!hasMemo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSend() {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_confirm_to_send_final)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class SendConfirmFragmentModule {
|
||||
@FragmentScope
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeFragment(): SendConfirmFragment
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
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.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
||||
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
|
||||
FragmentSendFinalBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onExit()
|
||||
}
|
||||
binding.backButtonHitArea.setOnClickListener {
|
||||
onExit()
|
||||
}
|
||||
binding.textConfirmation.text =
|
||||
"Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${mainActivity?.sendViewModel?.toAddress?.abbreviatedAddress()}"
|
||||
mainActivity?.sendViewModel?.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
|
||||
binding.radioIncludeAddress.isChecked = hasMemo
|
||||
binding.radioIncludeAddress.goneIf(!hasMemo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
mainActivity?.apply {
|
||||
sendViewModel.send(synchronizer).onEach {
|
||||
onPendingTxUpdated(it)
|
||||
}.launchIn(mainActivity?.lifecycleScope!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
flow {
|
||||
val max = binding.progressHorizontal.max - 1
|
||||
var progress = 0
|
||||
while (progress < max) {
|
||||
emit(progress)
|
||||
delay(Random.nextLong(1000))
|
||||
progress++
|
||||
}
|
||||
}.onEach {
|
||||
binding.progressHorizontal.progress = it
|
||||
}.launchIn(resumedScope)
|
||||
}
|
||||
|
||||
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
|
||||
val id = pendingTransaction?.id ?: -1
|
||||
var isSending = true
|
||||
val message = when {
|
||||
pendingTransaction == null -> "Transaction not found"
|
||||
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation . . ."
|
||||
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
|
||||
pendingTransaction.isCreating() -> "Creating transaction . . ."
|
||||
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
|
||||
}
|
||||
twig("Pending TX Updated: $message")
|
||||
binding.textStatus.apply {
|
||||
text = "$text\n$message"
|
||||
}
|
||||
binding.backButton.goneIf(!binding.textStatus.text.toString().contains("Awaiting"))
|
||||
binding.buttonNext.goneIf(isSending)
|
||||
binding.progressHorizontal.goneIf(!isSending)
|
||||
}
|
||||
|
||||
private fun onExit() {
|
||||
mainActivity?.navController?.popBackStack(R.id.send_navigation, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class SendFinalFragmentModule {
|
||||
@FragmentScope
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeFragment(): SendFinalFragment
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import cash.z.ecc.android.databinding.FragmentSendBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.onClickNavUp
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
class SendFragment : BaseFragment<FragmentSendBinding>() {
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendBinding =
|
||||
FragmentSendBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButtonHitArea.onClickNavUp()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class SendFragmentModule {
|
||||
@FragmentScope
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeFragment(): SendFragment
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.onClickNavBack
|
||||
import cash.z.ecc.android.ext.onClickNavUp
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendMemoBinding =
|
||||
FragmentSendMemoBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onAddMemo()
|
||||
}
|
||||
binding.buttonSkip.setOnClickListener {
|
||||
binding.inputMemo.setText("")
|
||||
mainActivity?.sendViewModel?.memo = ""
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
|
||||
}
|
||||
binding.backButtonHitArea.onClickNavBack()
|
||||
binding.radioIncludeAddress.setOnClickListener {
|
||||
if (binding.radioIncludeAddress.isActivated) {
|
||||
binding.radioIncludeAddress.isChecked = false
|
||||
} else {
|
||||
binding.radioIncludeAddress.isActivated = true
|
||||
}
|
||||
}
|
||||
binding.inputMemo.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onAddMemo()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
binding.radioIncludeAddress.requestFocus()
|
||||
}
|
||||
|
||||
private fun onAddMemo() {
|
||||
mainActivity?.sendViewModel?.memo = binding.inputMemo.text.toString()
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class SendMemoFragmentModule {
|
||||
@FragmentScope
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeFragment(): SendMemoFragment
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
|
||||
import cash.z.wallet.sdk.SdkSynchronizer
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendViewModel @Inject constructor(var lockBox: LockBox) : ViewModel() {
|
||||
fun send(synchronizer: Synchronizer): Flow<PendingTransaction> {
|
||||
val keys = (synchronizer as SdkSynchronizer).rustBackend!!.deriveSpendingKeys(
|
||||
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
|
||||
)
|
||||
return synchronizer.sendToAddress(
|
||||
keys[0],
|
||||
zatoshiAmount,
|
||||
toAddress,
|
||||
memo
|
||||
).onEach {
|
||||
twig(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
var toAddress: String = ""
|
||||
var memo: String = ""
|
||||
var zatoshiAmount: Long = -1L
|
||||
}
|
|
@ -15,18 +15,21 @@ import cash.z.ecc.android.R
|
|||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentBackupBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.onClick
|
||||
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
|
||||
import cash.z.ecc.android.feedback.measure
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
|
||||
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
||||
|
@ -79,7 +82,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
|
||||
}
|
||||
|
||||
private fun applySpan(vararg textViews: TextView) {
|
||||
private fun applySpan(vararg textViews: TextView) = lifecycleScope.launch {
|
||||
val words = loadSeedWords()
|
||||
val thinSpace = "\u2005" // 0.25 em space
|
||||
textViews.forEachIndexed { index, textView ->
|
||||
|
@ -92,11 +95,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadSeedWords(): List<CharArray> {
|
||||
private suspend fun loadSeedWords(): List<CharArray> = withContext(Dispatchers.IO) {
|
||||
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
|
||||
val lockBox = LockBox(ZcashWalletApp.instance)
|
||||
val mnemonics = Mnemonics()
|
||||
val seed = lockBox.getBytes(LockBoxKey.SEED)!!
|
||||
return mnemonics.nextMnemonicList(seed)
|
||||
val seedPhrase = lockBox.getCharsUtf8(LockBoxKey.SEED_PHRASE)!!
|
||||
val result = mnemonics.toWordList(seedPhrase)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,19 +12,17 @@ import cash.z.ecc.android.R
|
|||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentLandingBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.feedback.MetricType.SEED_CREATION
|
||||
import cash.z.ecc.android.feedback.measure
|
||||
import cash.z.ecc.android.isEmulator
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
||||
|
@ -46,6 +44,24 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
"backup" -> onBackupWallet()
|
||||
}
|
||||
}
|
||||
binding.buttonNegative.setOnLongClickListener {
|
||||
if (binding.buttonNegative.text.toString().toLowerCase() == "restore") {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setMessage("Would you like to import the dev wallet?\n\nIf so, please only send 0.0001 ZEC at a time and return some later so that the account remains funded.")
|
||||
.setTitle("Import Dev Wallet?")
|
||||
.setCancelable(true)
|
||||
.setPositiveButton("Import") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onUseDevWallet()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
binding.buttonNegative.setOnClickListener {
|
||||
when (binding.buttonNegative.text.toString().toLowerCase()) {
|
||||
"restore" -> onRestoreWallet()
|
||||
|
@ -84,24 +100,51 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
|
||||
private fun onRestoreWallet() {
|
||||
if (ZcashWalletApp.instance.isEmulator()) {
|
||||
onEnterWallet()
|
||||
} else {
|
||||
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun onUseDevWallet() {
|
||||
val seedPhrase =
|
||||
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
|
||||
val birthday = 663174//626599
|
||||
mainActivity?.apply {
|
||||
lifecycleScope.launch {
|
||||
initializeAccount(
|
||||
walletSetup.importWallet(feedback, seedPhrase.toCharArray()),
|
||||
Initializer.loadBirthdayFromAssets(ZcashWalletApp.instance, birthday)
|
||||
)
|
||||
initSync()
|
||||
}
|
||||
|
||||
binding.buttonPositive.isEnabled = true
|
||||
binding.textMessage.text = "Wallet imported! Congratulations!"
|
||||
binding.buttonNegative.text = "Skip"
|
||||
binding.buttonPositive.text = "Backup"
|
||||
playSound("sound_receive_small.mp3")
|
||||
vibrateSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move this to the ViewModel but doing so requires fixing dagger so do that as a separate PR
|
||||
private fun onNewWallet() {
|
||||
mainActivity?.feedback?.measure(SEED_CREATION) {
|
||||
walletSetup.createSeed()
|
||||
lifecycleScope.launch {
|
||||
val ogText = binding.buttonPositive.text
|
||||
binding.buttonPositive.text = "creating"
|
||||
binding.buttonPositive.isEnabled = false
|
||||
|
||||
mainActivity?.apply {
|
||||
initializeAccount(walletSetup.createWallet(feedback))
|
||||
initSync()
|
||||
}
|
||||
|
||||
binding.buttonPositive.isEnabled = true
|
||||
binding.textMessage.text = "Wallet created! Congratulations!"
|
||||
binding.buttonNegative.text = "Skip"
|
||||
binding.buttonPositive.text = "Backup"
|
||||
mainActivity?.playSound("sound_receive_small.mp3")
|
||||
mainActivity?.vibrateSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBackupWallet() {
|
||||
skipCount = 0
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
package cash.z.ecc.android.ui.setup
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.ecc.android.feedback.Feedback
|
||||
import cash.z.ecc.android.feedback.Report.MetricType.*
|
||||
import cash.z.ecc.android.feedback.measure
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val lockBox: LockBox) :
|
||||
|
@ -20,27 +22,80 @@ class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val loc
|
|||
|
||||
fun checkSeed(): Flow<WalletSetupState> = flow {
|
||||
when {
|
||||
lockBox.getBoolean(HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
|
||||
lockBox.getBoolean(HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
|
||||
lockBox.getBoolean(LockBoxKey.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
|
||||
lockBox.getBoolean(LockBoxKey.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
|
||||
else -> emit(NO_SEED)
|
||||
}
|
||||
}
|
||||
|
||||
fun createSeed() {
|
||||
check(!lockBox.getBoolean(HAS_SEED)) {
|
||||
/**
|
||||
* Take all the steps necessary to create a new wallet and measure how long it takes.
|
||||
*
|
||||
* @param feedback the object used for measurement.
|
||||
*/
|
||||
suspend fun createWallet(feedback: Feedback): ByteArray = withContext(Dispatchers.IO){
|
||||
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
|
||||
"Error! Cannot create a seed when one already exists! This would overwrite the" +
|
||||
" existing seed and could lead to a loss of funds if the user has no backup!"
|
||||
}
|
||||
|
||||
mnemonics.apply {
|
||||
lockBox.setBytes(SEED, nextSeed())
|
||||
lockBox.setBoolean(HAS_SEED, true)
|
||||
feedback.measure(WALLET_CREATED) {
|
||||
mnemonics.run {
|
||||
feedback.measure(ENTROPY_CREATED) { nextEntropy() }.let { entropy ->
|
||||
feedback.measure(SEED_PHRASE_CREATED) { nextMnemonic(entropy) }.let { seedPhrase ->
|
||||
feedback.measure(SEED_CREATED) { toSeed(seedPhrase) }.let { bip39Seed ->
|
||||
|
||||
lockBox.setCharsUtf8(LockBoxKey.SEED_PHRASE, seedPhrase)
|
||||
lockBox.setBoolean(LockBoxKey.HAS_SEED_PHRASE, true)
|
||||
|
||||
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
|
||||
lockBox.setBoolean(LockBoxKey.HAS_SEED, true)
|
||||
|
||||
bip39Seed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take all the steps necessary to import a wallet and measure how long it takes.
|
||||
*
|
||||
* @param feedback the object used for measurement.
|
||||
*/
|
||||
suspend fun importWallet(
|
||||
feedback: Feedback,
|
||||
seedPhrase: CharArray
|
||||
): ByteArray = withContext(Dispatchers.IO) {
|
||||
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
|
||||
"Error! Cannot import a seed when one already exists! This would overwrite the" +
|
||||
" existing seed and could lead to a loss of funds if the user has no backup!"
|
||||
}
|
||||
|
||||
feedback.measure(WALLET_IMPORTED) {
|
||||
mnemonics.run {
|
||||
feedback.measure(SEED_IMPORTED) { toSeed(seedPhrase) }.let { bip39Seed ->
|
||||
|
||||
lockBox.setCharsUtf8(LockBoxKey.SEED_PHRASE, seedPhrase)
|
||||
lockBox.setBoolean(LockBoxKey.HAS_SEED_PHRASE, true)
|
||||
|
||||
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
|
||||
lockBox.setBoolean(LockBoxKey.HAS_SEED, true)
|
||||
|
||||
bip39Seed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
object LockBoxKey {
|
||||
const val SEED = "cash.z.ecc.android.SEED1"
|
||||
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED1"
|
||||
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP1"
|
||||
const val SEED = "cash.z.ecc.android.SEED"
|
||||
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
|
||||
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
|
||||
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
|
||||
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:endColor="@color/colorPrimaryDark"
|
||||
android:startColor="@color/colorPrimary"
|
||||
android:type="radial"
|
||||
android:centerY="0.36"
|
||||
android:centerX="0.50"
|
||||
android:gradientRadius="640dp"/>
|
||||
</shape>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -4,6 +4,7 @@
|
|||
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
|
||||
|
@ -39,7 +40,7 @@
|
|||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="drum"
|
||||
tools:text="drum"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -51,7 +52,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7" />
|
||||
|
@ -61,7 +62,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_10" />
|
||||
|
@ -71,7 +72,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_7"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_13" />
|
||||
|
@ -81,7 +82,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_10"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_16" />
|
||||
|
@ -91,7 +92,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_13"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_19" />
|
||||
|
@ -101,7 +102,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_16"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_22" />
|
||||
|
@ -111,7 +112,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_19" />
|
||||
|
||||
|
@ -126,7 +127,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="fitness"
|
||||
tools:text="fitness"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
@ -137,7 +138,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
|
||||
|
@ -148,7 +149,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_11"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
|
||||
|
@ -159,7 +160,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_14"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_8" />
|
||||
|
@ -170,7 +171,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_17"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_11" />
|
||||
|
@ -181,7 +182,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_20"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_14" />
|
||||
|
@ -192,7 +193,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_23"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_17" />
|
||||
|
@ -203,7 +204,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_20" />
|
||||
|
||||
|
@ -218,7 +219,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="goals"
|
||||
tools:text="goals"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
@ -229,7 +230,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_9"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
|
||||
|
@ -240,7 +241,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_12"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
|
||||
|
@ -251,7 +252,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_15"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_9" />
|
||||
|
@ -262,7 +263,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_18"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_12" />
|
||||
|
@ -273,7 +274,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_21"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_15" />
|
||||
|
@ -284,7 +285,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_24"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_18" />
|
||||
|
@ -295,7 +296,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
tools:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_21" />
|
||||
<!--
|
||||
|
|
|
@ -13,28 +13,28 @@
|
|||
<!-- TODO: redo these keylines to match the designs, exactly -->
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_bottom_buttons"
|
||||
android:id="@+id/guideline_bottom_buttons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.7017784" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_start"
|
||||
android:id="@+id/guideline_keyline_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.054" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_end"
|
||||
android:id="@+id/guideline_keyline_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.946" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_bottom"
|
||||
android:id="@+id/guideline_keyline_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
|
@ -73,7 +73,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="Backup\nWallet"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
|
||||
app:layout_constraintTop_toTopOf="@id/back_button" />
|
||||
|
||||
<View
|
||||
|
@ -81,10 +81,10 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_banner_large"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/guieline_bottom_buttons"
|
||||
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"/>
|
||||
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"
|
||||
|
@ -97,8 +97,8 @@
|
|||
android:layout_marginStart="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
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
|
||||
|
@ -109,7 +109,7 @@
|
|||
android:text="View Logs"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_feedback"
|
||||
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"
|
||||
app:layout_constraintBottom_toBottomOf="@id/guideline_keyline_bottom"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
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">
|
||||
|
||||
<View
|
||||
|
@ -27,14 +28,28 @@
|
|||
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_send_amount_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.23" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_balance_available"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="12.34581242 ZEC"
|
||||
android:text="Updating"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
android:visibility="gone"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toStartOf="@id/label_balance"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -53,6 +68,17 @@
|
|||
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:visibility="gone"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
|
||||
|
||||
|
||||
<!-- -->
|
||||
<!-- Number Pad -->
|
||||
|
@ -213,17 +239,20 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/button_number_pad_9"
|
||||
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
|
||||
|
||||
<!-- TODO: properly style this as a button with ripples -->
|
||||
<TextView
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_send"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/background_button_rounded"
|
||||
android:gravity="center"
|
||||
style="@style/Zcash.Button"
|
||||
android:text="Send Amount"
|
||||
android:enabled="false"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="#000000"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guide_keys"
|
||||
app:layout_constraintStart_toStartOf="@id/guide_keys"
|
||||
app:layout_constraintTop_toBottomOf="@id/guide_keys"/>
|
||||
|
@ -234,6 +263,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:clickable="true"
|
||||
android:background="#D0000000"
|
||||
tools:visibility="gone"
|
||||
android:elevation="5dp" />
|
||||
|
||||
<!-- -->
|
||||
|
@ -322,36 +352,25 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/button_send"
|
||||
app:layout_constraintVertical_bias="@dimen/ratio_golden_small" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_zec_symbol"
|
||||
android:elevation="6dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:tint="@color/colorAccent"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_send_amount"
|
||||
app:layout_constraintEnd_toStartOf="@id/text_send_amount"
|
||||
app:layout_constraintHeight_percent="0.052"
|
||||
app:layout_constraintTop_toTopOf="@id/text_send_amount"
|
||||
app:layout_constraintVertical_bias="0.2"
|
||||
app:layout_constraintWidth_percent="0.060"
|
||||
app:srcCompat="@drawable/ic_zec_symbol" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_send_amount"
|
||||
android:elevation="6dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:text="$0"
|
||||
android:textAppearance="@style/Zcash.TextAppearance.Zec"
|
||||
android:textSize="72dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/guide_keys"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
|
||||
app:autoSizeTextType="uniform"
|
||||
app:layout_constraintBottom_toTopOf="@id/guideline_send_amount_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
app:layout_constraintTop_toTopOf="@id/guideline_send_amount_top" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Banner -->
|
||||
|
@ -379,7 +398,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="Learn More"
|
||||
android:text="Fund Now"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/colorPrimary"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
|
||||
|
@ -395,6 +414,7 @@
|
|||
android:id="@+id/group_banner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="text_banner_message, text_banner_action" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -49,7 +49,7 @@
|
|||
android:gravity="center"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Welcome to the Birch Wallet!"
|
||||
android:text="Welcome to the zECC Wallet!"
|
||||
android:textColor="@color/zcashWhite"
|
||||
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
<?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"
|
||||
android:background="@drawable/background_home"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/text_light"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:layout_constraintHorizontal_bias="0.05"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.01"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_light"
|
||||
android:text="SCAN"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,154 @@
|
|||
<?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"
|
||||
android:background="@drawable/background_home"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/text_light"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:layout_constraintHorizontal_bias="0.05"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.01"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_amount"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
tools:text="Sending 12.34121212 ZEC"
|
||||
android:textColor="@color/text_light"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/back_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/back_button" />
|
||||
|
||||
<!-- Input: Address -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:hint="@string/send_hint_input_zcash_address"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:paddingRight="76dp"
|
||||
android:singleLine="true"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="@color/text_light"
|
||||
app:backgroundTint="@color/colorPrimary"
|
||||
android:textColorHint="@color/text_light_dimmed"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.84"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@android:color/holo_red_light"
|
||||
android:textSize="12dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/input_zcash_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/input_zcash_address"
|
||||
tools:text="invalid address" />
|
||||
|
||||
<!-- Scan QR code -->
|
||||
<ImageView
|
||||
android:id="@+id/image_scan_qr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="1dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:tint="@color/zcashWhite"
|
||||
app:layout_constraintBottom_toBottomOf="@id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/input_zcash_address"
|
||||
app:layout_constraintTop_toTopOf="@id/input_zcash_address"
|
||||
app:srcCompat="@drawable/ic_qrcode_24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Next"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
|
||||
app:layout_constraintTop_toBottomOf="@+id/input_zcash_address" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Banner -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_banner_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_banner"
|
||||
android:elevation="6dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:text="Address on clipboard!"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
|
||||
app:layout_constraintStart_toStartOf="@+id/input_zcash_address"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.45"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_banner_action"
|
||||
android:elevation="6dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="Paste"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/colorPrimary"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_banner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="text_banner_message, text_banner_action"
|
||||
android:visibility="visible"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,90 @@
|
|||
<?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">
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.05"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.01"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_confirmation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline4"
|
||||
android:gravity="center"
|
||||
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?"
|
||||
android:textColor="@color/text_light"
|
||||
android:maxLines="3"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.21"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_include_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Includes memo"
|
||||
android:enabled="false"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+.0001 Network Fee"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.84"
|
||||
/>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="Tap\nto send ZEC"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.48" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,121 @@
|
|||
<?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_send_final"
|
||||
android:animateLayoutChanges="true"
|
||||
>
|
||||
|
||||
<View
|
||||
android:id="@+id/guide_keys"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0.3"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.38196601125"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.58"
|
||||
app:layout_constraintWidth_percent="0.7475728155" />
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.05"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:srcCompat="@drawable/ic_close_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.01"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_confirmation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline4"
|
||||
android:gravity="center"
|
||||
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?"
|
||||
android:textColor="@color/text_dark"
|
||||
android:maxLines="3"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.21"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_dark"
|
||||
tools:text="Creating transaction..."
|
||||
android:textSize="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/radio_include_address"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.1"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintStart_toStartOf="@id/text_status"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_status"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_status"
|
||||
android:indeterminate="false"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:max="150"
|
||||
android:foregroundTint="@color/zcashBlack_87" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_include_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:text="Includes memo"
|
||||
android:enabled="false"
|
||||
android:textColor="@color/text_dark_dimmed"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
style="@style/Zcash.Button.OutlinedButton"
|
||||
app:strokeColor="@color/text_dark"
|
||||
android:padding="12dp"
|
||||
android:text="Finished"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@id/guide_keys"
|
||||
app:layout_constraintStart_toStartOf="@id/guide_keys"
|
||||
app:layout_constraintTop_toBottomOf="@id/guide_keys" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,110 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_home">
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.05"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.01"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_memo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_banner"
|
||||
android:elevation="6dp"
|
||||
android:gravity="top"
|
||||
android:imeActionLabel="add memo"
|
||||
android:inputType="textImeMultiLine"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="Add a memo here"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.185"
|
||||
app:layout_constraintWidth_percent="0.8"
|
||||
tools:text="This is my memo" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_include_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Include your sending address in memo"
|
||||
app:layout_constraintStart_toStartOf="@+id/input_memo"
|
||||
app:layout_constraintTop_toBottomOf="@+id/input_memo" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="Your transaction is shielded and your address is unavailable to recipient"
|
||||
app:layout_constraintEnd_toEndOf="@id/input_memo"
|
||||
app:layout_constraintStart_toStartOf="@id/input_memo"
|
||||
app:layout_constraintTop_toBottomOf="@id/radio_include_address" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:padding="12dp"
|
||||
android:text="Add Memo"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.8"
|
||||
app:layout_constraintWidth_percent="0.68" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_skip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Zcash.Button.OutlinedButton"
|
||||
android:padding="12dp"
|
||||
android:text="Send without memo"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintEnd_toEndOf="@id/button_next"
|
||||
app:layout_constraintStart_toStartOf="@id/button_next"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_next" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -13,22 +13,17 @@
|
|||
<action
|
||||
android:id="@+id/action_nav_home_to_nav_receive"
|
||||
app:destination="@id/nav_receive" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_nav_send"
|
||||
app:destination="@id/nav_send" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_nav_detail"
|
||||
app:destination="@id/nav_detail" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_create_wallet"
|
||||
app:destination="@id/wallet_setup_navigation" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_send"
|
||||
app:destination="@id/send_navigation" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_send"
|
||||
android:name="cash.z.ecc.android.ui.send.SendFragment"
|
||||
tools:layout="@layout/fragment_send" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_receive"
|
||||
android:name="cash.z.ecc.android.ui.receive.ReceiveFragment"
|
||||
|
@ -44,5 +39,6 @@
|
|||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/wallet_setup_navigation" />
|
||||
<include app:graph="@navigation/send_navigation" />
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation
|
||||
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/send_navigation"
|
||||
app:startDestination="@+id/nav_send_address">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_send_address"
|
||||
android:name="cash.z.ecc.android.ui.send.SendAddressFragment"
|
||||
tools:layout="@layout/fragment_send_address" >
|
||||
<action
|
||||
android:id="@+id/action_nav_send_address_to_send_memo"
|
||||
app:destination="@id/nav_send_memo"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_send_memo"
|
||||
android:name="cash.z.ecc.android.ui.send.SendMemoFragment"
|
||||
tools:layout="@layout/fragment_send_memo" >
|
||||
<action
|
||||
android:id="@+id/action_nav_send_memo_to_send_confirm"
|
||||
app:destination="@id/nav_send_confirm"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_send_confirm"
|
||||
android:name="cash.z.ecc.android.ui.send.SendConfirmFragment"
|
||||
tools:layout="@layout/fragment_send_confirm" >
|
||||
<action
|
||||
android:id="@+id/action_nav_send_confirm_to_send_final"
|
||||
app:destination="@id/nav_send_final"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_send_final"
|
||||
android:name="cash.z.ecc.android.ui.send.SendFinalFragment"
|
||||
tools:layout="@layout/fragment_send_final" >
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
|
@ -12,7 +12,9 @@
|
|||
tools:layout="@layout/fragment_landing" >
|
||||
<action
|
||||
android:id="@+id/action_nav_landing_to_nav_backup"
|
||||
app:destination="@id/nav_backup" />
|
||||
app:destination="@id/nav_backup"
|
||||
app:popUpTo="@id/nav_landing"
|
||||
app:popUpToInclusive="true"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
<color name="zcashWhite">#F5F5F5</color>
|
||||
<color name="zcashWhite_12">#1FFFFFFF</color>
|
||||
<color name="zcashWhite_24">#3DFFFFFF</color>
|
||||
<color name="zcashWhite_40">#66FFFFFF</color>
|
||||
<color name="zcashWhite_50">#80FFFFFF</color>
|
||||
<color name="zcashWhite_60">#A3FFFFFF</color>
|
||||
|
|
|
@ -4,4 +4,7 @@
|
|||
|
||||
|
||||
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
|
||||
|
||||
<!-- Send Flow -->
|
||||
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
|
||||
</resources>
|
||||
|
|
|
@ -26,11 +26,15 @@
|
|||
<item name="android:background">@drawable/selector_pressed_ripple_circle</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.Button.White" parent="Widget.MaterialComponents.Button">
|
||||
<item name="backgroundTint">@android:color/white</item>
|
||||
<style name="Zcash.Button" parent="Widget.MaterialComponents.Button">
|
||||
<item name="backgroundTint">@color/colorPrimary</item>
|
||||
<item name="android:textColor">@color/text_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.Button.White">
|
||||
<item name="backgroundTint">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.Button.OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
|
||||
<item name="strokeColor">@color/zcashWhite</item>
|
||||
</style>
|
||||
|
@ -65,7 +69,7 @@
|
|||
|
||||
<style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
|
||||
<item name="cornerFamily">cut</item>
|
||||
<item name="cornerSize">8dp</item>
|
||||
<item name="cornerSize">10dp</item>
|
||||
</style>
|
||||
<style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
|
||||
<item name="cornerFamily">cut</item>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package cash.z.ecc.android
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ScratchPad {
|
||||
|
||||
val t get() = System.currentTimeMillis()
|
||||
var t0 = 0L
|
||||
val Δt get() = t - t0
|
||||
|
||||
@Test
|
||||
fun testMarblesCombine() = runBlocking {
|
||||
var started = false
|
||||
val flow = flowOf(1, 2, 3, 4, 5, 6, 7, 8, 9).onEach {
|
||||
delay(100)
|
||||
if (!started) {
|
||||
t0 = t
|
||||
started = true
|
||||
}
|
||||
println("$Δt\temitting $it");
|
||||
}
|
||||
val flow2 = flowOf("a", "b", "c", "d", "e", "f").onEach { delay(150); println("$Δt\temitting $it")}
|
||||
val flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it")}
|
||||
combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart {
|
||||
t0 = t
|
||||
}.collect {
|
||||
// if (!started) {
|
||||
// println("$Δt until first emission")
|
||||
// t0 = t
|
||||
// started = true
|
||||
// }
|
||||
println("$Δt\t$it") // Will print "1a 2a 2b 2c"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMarblesScan() = runBlocking {
|
||||
val flow = flowOf(1, 2, 3, 4, 5)
|
||||
|
||||
flow.scanReduce { accumulator, value ->
|
||||
println("was: $accumulator now: $value")
|
||||
value
|
||||
}.collect {
|
||||
println("got $it")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -92,12 +92,13 @@ class Feedback(capacity: Int = 256) {
|
|||
* will run concurrently--meaning a "happens before" relationship between the measurer and the
|
||||
* measured cannot be established and thereby the concurrent action cannot be timed.
|
||||
*/
|
||||
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T) {
|
||||
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T): T {
|
||||
ensureScope()
|
||||
val metric = TimeMetric(key, description.toString()).markTime()
|
||||
block()
|
||||
val result = block()
|
||||
metric.markTime()
|
||||
report(metric)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,8 @@ class LockBox @Inject constructor(private val appContext: Context) : LockBoxProv
|
|||
}
|
||||
|
||||
override fun setBytes(key: String, value: ByteArray) {
|
||||
// using hex here because this library doesn't really work well for byte arrays
|
||||
// but hopefully we can code to arrays and then change the underlying library, later
|
||||
SecurePreferences.setValue(appContext, key, value.toHex())
|
||||
}
|
||||
|
||||
|
@ -27,11 +29,13 @@ class LockBox @Inject constructor(private val appContext: Context) : LockBoxProv
|
|||
}
|
||||
|
||||
override fun setCharsUtf8(key: String, value: CharArray) {
|
||||
setBytes(key, value.toBytes())
|
||||
// Using string here because this library doesn't work well for char arrays
|
||||
// but hopefully we can code to arrays and then change the underlying library, later
|
||||
SecurePreferences.setValue(appContext, key, String(value))
|
||||
}
|
||||
|
||||
override fun getCharsUtf8(key: String): CharArray? {
|
||||
return getBytes(key)?.fromBytes()
|
||||
return SecurePreferences.getStringValue(appContext, key, null)?.toCharArray()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ interface MnemonicProvider {
|
|||
/**
|
||||
* Generate a random seed.
|
||||
*/
|
||||
fun nextSeed(): ByteArray
|
||||
fun nextEntropy(): ByteArray
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase.
|
||||
|
|
|
@ -12,19 +12,19 @@ import javax.inject.Inject
|
|||
// which expects a string so for that reason, we just use Strings here)
|
||||
class Mnemonics @Inject constructor(): MnemonicProvider {
|
||||
|
||||
override fun nextSeed(): ByteArray {
|
||||
override fun nextEntropy(): ByteArray {
|
||||
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextMnemonic(): CharArray {
|
||||
return nextMnemonic(nextSeed())
|
||||
return nextMnemonic(nextEntropy())
|
||||
}
|
||||
|
||||
override fun nextMnemonic(seed: ByteArray): CharArray {
|
||||
override fun nextMnemonic(entropy: ByteArray): CharArray {
|
||||
return StringBuilder().let { builder ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.toString().toCharArray()
|
||||
|
@ -32,12 +32,12 @@ class Mnemonics @Inject constructor(): MnemonicProvider {
|
|||
}
|
||||
|
||||
override fun nextMnemonicList(): List<CharArray> {
|
||||
return nextMnemonicList(nextSeed())
|
||||
return nextMnemonicList(nextEntropy())
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(seed: ByteArray): List<CharArray> {
|
||||
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> {
|
||||
return WordListBuilder().let { builder ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.wordList
|
||||
|
@ -46,7 +46,7 @@ class Mnemonics @Inject constructor(): MnemonicProvider {
|
|||
|
||||
override fun toSeed(mnemonic: CharArray): ByteArray {
|
||||
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
|
||||
return SeedCalculator().calculateSeed(mnemonic.toString(), "")
|
||||
return SeedCalculator().calculateSeed(String(mnemonic), "")
|
||||
}
|
||||
|
||||
override fun toWordList(mnemonic: CharArray): List<CharArray> {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
rootProject.name='Zcash Wallet'
|
||||
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox'
|
||||
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':sdk'
|
||||
project(":sdk").projectDir = file("../zcash-android-wallet-sdk")
|
Loading…
Reference in New Issue