diff --git a/app/build.gradle b/app/build.gradle index 9efe529..ffeb025 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ apply plugin: 'com.google.firebase.firebase-perf' archivesBaseName = 'zcash-android-wallet' group = 'cash.z.ecc.android' -version = '1.0.0-alpha23' +version = '1.0.0-alpha25' android { compileSdkVersion Deps.compileSdkVersion @@ -21,7 +21,7 @@ android { applicationId 'cash.z.ecc.android' minSdkVersion Deps.minSdkVersion targetSdkVersion Deps.targetSdkVersion - versionCode = 1_00_00_023 + versionCode = 1_00_00_025 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX) dev(9XX). 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' diff --git a/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt b/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt index 7f462f3..8af5e00 100644 --- a/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt +++ b/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt @@ -17,7 +17,7 @@ class SynchronizerModule { @Provides @SynchronizerScope fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer { - return Synchronizer(appContext, initializer) + return Synchronizer(initializer) } } diff --git a/app/src/main/java/cash/z/ecc/android/ext/Spannable.kt b/app/src/main/java/cash/z/ecc/android/ext/Spannable.kt index 1d361a5..29247c7 100644 --- a/app/src/main/java/cash/z/ecc/android/ext/Spannable.kt +++ b/app/src/main/java/cash/z/ecc/android/ext/Spannable.kt @@ -5,7 +5,7 @@ import android.text.Spanned import android.text.style.ForegroundColorSpan import androidx.core.text.toSpannable -fun String.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable { +fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable { return toSpannable().apply { val start = this@toColoredSpan.indexOf(coloredPortion) setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/app/src/main/java/cash/z/ecc/android/ext/View.kt b/app/src/main/java/cash/z/ecc/android/ext/View.kt index 62b9842..4bfec90 100644 --- a/app/src/main/java/cash/z/ecc/android/ext/View.kt +++ b/app/src/main/java/cash/z/ecc/android/ext/View.kt @@ -22,6 +22,10 @@ fun View.disabledIf(isDisabled: Boolean) { isEnabled = !isDisabled } +fun View.transparentIf(isTransparent: Boolean) { + alpha = if (isTransparent) 0.0f else 1.0f +} + fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) { setOnClickListener { block() diff --git a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt index 3ec359a..b6899b3 100644 --- a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt +++ b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt @@ -43,6 +43,12 @@ object Report { object ImportCompleted : Restore("importcompleted", 50) object Success : Restore("success", 100) } + + sealed class UserFeedback(stepName: String, step: Int, vararg properties: Pair) : Feedback.Funnel("feedback", stepName, step, *properties) { + object Started : UserFeedback("started", 0) + object Cancelled : UserFeedback("cancelled", 1) + class Submitted(rating: Int, question1: String, question2: String, question3: String) : UserFeedback("submitted", 100, "rating" to rating, "question1" to question1, "question2" to question2, "question3" to question3) + } } object Error { @@ -84,6 +90,7 @@ object Report { DETAIL("wallet.detail"), LANDING, PROFILE, + FEEDBACK, RECEIVE, RESTORE, SCAN, @@ -120,6 +127,8 @@ object Report { PROFILE_VIEW_USER_LOGS("profile.view.user.logs"), PROFILE_VIEW_DEV_LOGS("profile.view.dev.logs"), PROFILE_SEND_FEEDBACK("profile.send.feedback"), + FEEDBACK_CANCEL("feedback.cancel"), + FEEDBACK_SUBMIT("feedback.submit"), RECEIVE_SCAN("receive.scan"), RECEIVE_BACK("receive.back"), RESTORE_DONE("restore.done"), diff --git a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt index 8337d21..d46ab6e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt @@ -50,7 +50,6 @@ import javax.inject.Inject class MainActivity : AppCompatActivity() { - @Inject lateinit var feedback: Feedback @@ -324,6 +323,7 @@ class MainActivity : AppCompatActivity() { showSnackbar("Well, this is awkward. You denied permission for the camera.") } + // TODO: clean up this error handling private var ignoredErrors = 0 private fun onProcessorError(error: Throwable?): Boolean { var notified = false @@ -368,25 +368,25 @@ class MainActivity : AppCompatActivity() { } if (!notified) { ignoredErrors++ - } - if (ignoredErrors >= ZcashSdk.RETRIES) { - if (dialog == null) { - notified = true - runOnUiThread { - dialog = MaterialAlertDialogBuilder(this) - .setTitle("Processor Error") - .setMessage(error?.message ?: "Critical error while processing blocks!") - .setCancelable(false) - .setPositiveButton("Retry") { d, _ -> - d.dismiss() - dialog = null - } - .setNegativeButton("Exit") { dialog, _ -> - dialog.dismiss() - throw error - ?: RuntimeException("Critical error while processing blocks and the user chose to exit.") - } - .show() + if (ignoredErrors >= ZcashSdk.RETRIES) { + if (dialog == null) { + notified = true + runOnUiThread { + dialog = MaterialAlertDialogBuilder(this) + .setTitle("Processor Error") + .setMessage(error?.message ?: "Critical error while processing blocks!") + .setCancelable(false) + .setPositiveButton("Retry") { d, _ -> + d.dismiss() + dialog = null + } + .setNegativeButton("Exit") { dialog, _ -> + dialog.dismiss() + throw error + ?: RuntimeException("Critical error while processing blocks and the user chose to exit.") + } + .show() + } } } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt index 8c43d56..3bca6c7 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt @@ -2,11 +2,15 @@ package cash.z.ecc.android.ui.detail import android.view.View import android.widget.TextView +import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import cash.z.ecc.android.R import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.toAppColor import cash.z.ecc.android.ui.MainActivity +import cash.z.ecc.android.ui.send.SendViewModel +import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX +import cash.z.ecc.android.ui.util.toUtf8Memo import cash.z.wallet.sdk.entity.ConfirmedTransaction import cash.z.wallet.sdk.ext.* import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -21,6 +25,7 @@ class TransactionViewHolder(itemView: View) : Recycler private val bottomText = itemView.findViewById(R.id.text_transaction_bottom) private val shieldIcon = itemView.findViewById(R.id.image_shield) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) + private val addressRegex = """zs\d\w{65,}""".toRegex() fun bindTo(transaction: T?) { @@ -29,13 +34,19 @@ class TransactionViewHolder(itemView: View) : Recycler var lineTwo: String = "" var amountZec: String = "" var amountDisplay: String = "" - var amountColor: Int = 0 - var indicatorBackground: Int = 0 + var amountColor: Int = R.color.text_light_dimmed + var lineOneColor: Int = R.color.text_light + var lineTwoColor: Int = R.color.text_light_dimmed + var indicatorBackground: Int = R.drawable.background_indicator_unknown transaction?.apply { itemView.setOnClickListener { onTransactionClicked(this) } + itemView.setOnLongClickListener { + onTransactionLongPressed(this) + true + } amountZec = value.convertZatoshiToZecString() // TODO: these might be good extension functions val timestamp = formatter.format(blockTimeInSeconds * 1000L) @@ -45,11 +56,16 @@ class TransactionViewHolder(itemView: View) : Recycler lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" amountDisplay = "- $amountZec" - amountColor = R.color.zcashRed - indicatorBackground = R.drawable.background_indicator_outbound + if (isMined) { + amountColor = R.color.zcashRed + indicatorBackground = R.drawable.background_indicator_outbound + } else { + lineOneColor = R.color.text_light_dimmed + lineTwoColor = R.color.text_light + } } - raw == null || raw?.isEmpty() == true -> { - lineOne = "Unknown paid you" + toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> { + lineOne = getSender(transaction) lineTwo = "Received $timestamp" amountDisplay = "+ $amountZec" amountColor = R.color.zcashGreen @@ -58,9 +74,10 @@ class TransactionViewHolder(itemView: View) : Recycler else -> { lineOne = "Unknown" lineTwo = "Unknown" + amountDisplay = "$amountZec" + amountColor = R.color.text_light } } - // sanitize amount if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001" else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal @@ -73,17 +90,41 @@ class TransactionViewHolder(itemView: View) : Recycler bottomText.text = lineTwo amountText.text = amountDisplay amountText.setTextColor(amountColor.toAppColor()) + topText.setTextColor(lineOneColor.toAppColor()) + bottomText.setTextColor(lineTwoColor.toAppColor()) val context = itemView.context indicator.background = context.resources.getDrawable(indicatorBackground) shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) } + private fun getSender(transaction: ConfirmedTransaction): String { + val memo = transaction.memo.toUtf8Memo() + return when { + memo.contains(INCLUDE_MEMO_PREFIX) -> { + val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim() + "${address.toAbbreviatedAddress()} paid you" + } + memo.contains("eply to:") -> { + val address = memo.split("eply to:")[1].trim() + "${address.toAbbreviatedAddress()} paid you" + } + memo.contains("zs") -> { + val who = extractAddress(memo)?.toAbbreviatedAddress() ?: "Unknown" + "$who paid you" + } + else -> "Unknown paid you" + } + } + + private fun extractAddress(memo: String?) = + addressRegex.findAll(memo ?: "").lastOrNull()?.value + private fun onTransactionClicked(transaction: ConfirmedTransaction) { val txId = transaction.rawTransactionId.toTxId() val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" + "Transaction: $txId" + - "${if (transaction.toAddress != null) "\n\nto: ${transaction.toAddress}" else ""}" + - "${if (transaction.memo != null) "\n\nmemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}" + "${if (transaction.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" + + "${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}" MaterialAlertDialogBuilder(itemView.context) .setMessage(detailsMessage) @@ -98,6 +139,12 @@ class TransactionViewHolder(itemView: View) : Recycler } .show() } + + private fun onTransactionLongPressed(transaction: ConfirmedTransaction) { + (transaction.toAddress ?: extractAddress(transaction.memo.toUtf8Memo()))?.let { + (itemView.context as MainActivity).copyText(it, "Transaction Address") + } + } } private fun ByteArray.toTxId(): String { diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt index 3da050d..9305458 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt @@ -66,6 +66,7 @@ class WalletDetailFragment : BaseFragment() { adapter = TransactionAdapter() viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) } binding.recyclerTransactions.adapter = adapter + binding.recyclerTransactions.smoothScrollToPosition(0) } private fun onTransactionsUpdated(transactions: PagedList) { diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt index 44f62e4..078a376 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt @@ -19,7 +19,8 @@ import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.Synchronizer.Status.SYNCED +import cash.z.wallet.sdk.Synchronizer.Status.* +import cash.z.wallet.sdk.block.CompactBlockProcessor import cash.z.wallet.sdk.ext.convertZatoshiToZecString import cash.z.wallet.sdk.ext.convertZecToZatoshi import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal @@ -92,7 +93,6 @@ class HomeFragment : BaseFragment() { buttonNumberPadBack.asKey() ) hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) } - iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) } textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) } hitAreaScan.setOnClickListener { mainActivity?.maybeOpenScan().also { tapped(HOME_SCAN) } @@ -195,8 +195,8 @@ class HomeFragment : BaseFragment() { } fun setProgress(uiModel: HomeViewModel.UiModel) { - if (!uiModel.processorInfo.hasData) { - twig("Warning: ignoring progress update because the processor has not started.") + if (!uiModel.processorInfo.hasData && !uiModel.isDisconnected) { + twig("Warning: ignoring progress update because the processor is still starting.") return } @@ -207,28 +207,21 @@ class HomeFragment : BaseFragment() { } val sendText = when { + uiModel.status == DISCONNECTED -> "Reconnecting . . ." uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE" - uiModel.status == Synchronizer.Status.DISCONNECTED -> "DISCONNECTED" - uiModel.status == Synchronizer.Status.STOPPED -> "IDLE" + uiModel.status == STOPPED -> "IDLE" uiModel.isDownloading -> "Downloading . . . ${snake.downloadProgress}%" uiModel.isValidating -> "Validating . . ." uiModel.isScanning -> "Scanning . . . ${snake.scanProgress}%" else -> "Updating" } -// binding.lottieButtonLoading.progress = if (uiModel.isSynced) 1.0f else uiModel.totalProgress * 0.82f // line fully closes at 82% mark binding.buttonSendAmount.text = sendText -// twig("Lottie progress set to ${binding.lottieButtonLoading.progress} (isSynced? ${uiModel.isSynced})") twig("Send button set to: $sendText") val resId = if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light binding.buttonSendAmount.setTextColor(resources.getColorStateList(resId)) - -// if (uiModel.status == DISCONNECTED || uiModel.status == STOPPED) { -// binding.buttonSendAmount.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.zcashGray)) -// } else { -// binding.buttonSendAmount.backgroundTintList = null -// } + binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected) } /** @@ -243,10 +236,13 @@ class HomeFragment : BaseFragment() { } fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) { - val availableString = if (availableBalance < 0) "Updating" else availableBalance.convertZatoshiToZecString() + val missingBalance = availableBalance < 0 + val availableString = if (missingBalance) "Updating" else availableBalance.convertZatoshiToZecString() binding.textBalanceAvailable.text = availableString + binding.textBalanceAvailable.transparentIf(missingBalance) + binding.labelBalance.transparentIf(missingBalance) binding.textBalanceDescription.apply { - goneIf(availableBalance < 0) + goneIf(missingBalance) text = if (availableBalance != -1L && (availableBalance < totalBalance)) { val change = (totalBalance - availableBalance).convertZatoshiToZecString() "(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change") @@ -275,12 +271,10 @@ class HomeFragment : BaseFragment() { private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) { logUpdate(old, new) - if (binding.lottieButtonLoading.visibility != View.VISIBLE) binding.lottieButtonLoading.visibility = View.VISIBLE uiModel = new if (old?.pendingSend != new.pendingSend) { setSendAmount(new.pendingSend) } - // TODO: handle stopped and disconnected flows setProgress(uiModel) // TODO: we may not need to separate anymore // if (new.status = SYNCING) onSyncing(new) else onSynced(new) if (new.status == SYNCED) onSynced(new) else onSyncing(new) diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt index 968cde2..63395be 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt @@ -21,8 +21,7 @@ class HomeViewModel @Inject constructor() : ViewModel() { lateinit var uiModels: Flow - private val _typedChars = ConflatedBroadcastChannel() - private val typedChars = _typedChars.asFlow() + lateinit var _typedChars: ConflatedBroadcastChannel var initialized = false @@ -32,12 +31,19 @@ class HomeViewModel @Inject constructor() : ViewModel() { twig("Warning already initialized HomeViewModel. Ignoring call to initialize.") return } + + if (::_typedChars.isInitialized) { + _typedChars.close() + } + _typedChars = ConflatedBroadcastChannel() + val typedChars = _typedChars.asFlow() + 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 ") + || (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c") acc } c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars") @@ -96,6 +102,7 @@ class HomeViewModel @Inject constructor() : ViewModel() { val isDownloading = status == DOWNLOADING val isScanning = status == SCANNING val isValidating = status == VALIDATING + val isDisconnected = status == DISCONNECTED val downloadProgress: Int get() { return processorInfo.run { if (lastDownloadRange.isEmpty()) { diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt b/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt index 01d54d5..57f812e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt @@ -7,7 +7,7 @@ import com.airbnb.lottie.LottieAnimationView class MagicSnakeLoader( val lottie: LottieAnimationView, private val scanningStartFrame: Int = 100, - private val scanningEndFrame: Int = 175, + private val scanningEndFrame: Int = 187, val totalFrames: Int = 200 ) : ValueAnimator.AnimatorUpdateListener { private var isPaused: Boolean = true diff --git a/app/src/main/java/cash/z/ecc/android/ui/profile/FeedbackFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/profile/FeedbackFragment.kt new file mode 100644 index 0000000..c8c75d7 --- /dev/null +++ b/app/src/main/java/cash/z/ecc/android/ui/profile/FeedbackFragment.kt @@ -0,0 +1,92 @@ +package cash.z.ecc.android.ui.profile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowManager +import android.widget.Toast +import androidx.core.view.doOnLayout +import cash.z.ecc.android.databinding.FragmentFeedbackBinding +import cash.z.ecc.android.di.viewmodel.viewModel +import cash.z.ecc.android.feedback.Report +import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback +import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_CANCEL +import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_SUBMIT +import cash.z.ecc.android.ui.base.BaseFragment + + +/** + * Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the + * application. + */ +class FeedbackFragment : BaseFragment() { + override val screen = Report.Screen.FEEDBACK + + override fun inflate(inflater: LayoutInflater): FragmentFeedbackBinding = + FragmentFeedbackBinding.inflate(inflater) + + private lateinit var ratings: Array + +// private val padder = ViewTreeObserver.OnGlobalLayoutListener { +// Toast.makeText(mainActivity, "LAYOUT", Toast.LENGTH_SHORT).show() +// } + + // + // LifeCycle + // + + override fun onResume() { + super.onResume() +// mainActivity!!.window.decorView.viewTreeObserver.addOnGlobalLayoutListener(padder) +// mainActivity!!.findViewById(android.R.id.content).viewTreeObserver.addOnGlobalLayoutListener(padder) + } + + override fun onPause() { + super.onPause() +// mainActivity!!.window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(padder) +// mainActivity!!.findViewById(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(padder) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding) { + backButtonHitArea.setOnClickListener(::onFeedbackCancel) + buttonSubmit.setOnClickListener(::onFeedbackSubmit) + + ratings = arrayOf(feedbackExp1, feedbackExp2, feedbackExp3, feedbackExp4, feedbackExp5) + ratings.forEach { + it.setOnClickListener(::onRatingClicked) + } + } + } + + + // + // Private API + // + + private fun onFeedbackSubmit(view: View) { + Toast.makeText(mainActivity, "Thanks for the feedback!", Toast.LENGTH_LONG).show() + tapped(FEEDBACK_SUBMIT) + + val q1 = binding.inputQuestion1.editText?.text.toString() + val q2 = binding.inputQuestion2.editText?.text.toString() + val q3 = binding.inputQuestion3.editText?.text.toString() + val rating = ratings.indexOfFirst { it.isActivated } + 1 + + mainActivity?.reportFunnel(UserFeedback.Submitted(rating, q1, q2, q3)) + + mainActivity?.navController?.navigateUp() + } + private fun onFeedbackCancel(view: View) { + tapped(FEEDBACK_CANCEL) + mainActivity?.reportFunnel(UserFeedback.Cancelled) + mainActivity?.navController?.navigateUp() + } + + private fun onRatingClicked(view: View) { + ratings.forEach { it.isActivated = false } + view.isActivated = !view.isActivated + } +} diff --git a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt index e59fa5b..4d4088a 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt @@ -1,6 +1,5 @@ package cash.z.ecc.android.ui.profile -import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle @@ -17,6 +16,7 @@ import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.ext.onClickNavTo import cash.z.ecc.android.feedback.FeedbackFile import cash.z.ecc.android.feedback.Report +import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment import cash.z.wallet.sdk.ext.toAbbreviatedAddress @@ -39,6 +39,11 @@ class ProfileFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) binding.hitAreaClose.onClickNavBack() { tapped(PROFILE_CLOSE) } binding.buttonBackup.onClickNavTo(R.id.action_nav_profile_to_nav_backup) { tapped(PROFILE_BACKUP) } + binding.buttonFeedback.onClickNavTo(R.id.action_nav_profile_to_nav_feedback) { + tapped(PROFILE_SEND_FEEDBACK) + mainActivity?.reportFunnel(UserFeedback.Started) + Unit + } binding.textVersion.text = BuildConfig.VERSION_NAME onClick(binding.buttonLogs) { tapped(PROFILE_VIEW_USER_LOGS) @@ -49,10 +54,6 @@ class ProfileFragment : BaseFragment() { onViewDevLogs() true } - onClick(binding.buttonFeedback) { - tapped(PROFILE_SEND_FEEDBACK) - onSendFeedback() - } } override fun onResume() { @@ -95,10 +96,6 @@ class ProfileFragment : BaseFragment() { startActivity(Intent.createChooser(intent, "Share Log File")) } - private fun onSendFeedback() { - mainActivity?.showSnackbar("Feedback feature coming soon!") - } - private fun userLogFile(): File? { return mainActivity?.feedbackCoordinator?.findObserver()?.file } diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt index a7d9c70..c13ab4e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt @@ -54,7 +54,7 @@ class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Uni private fun onImageScan(result: List, image: ImageProxy) { result.firstOrNull()?.rawValue?.let { scanCallback(it, image) - } ?: image.close() + } ?: runCatching { image.close() } } private fun onImageScanFailure(e: Exception) { diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt index 484506a..d8a9526 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt @@ -150,8 +150,8 @@ class SendAddressFragment : BaseFragment(), private fun onBalanceUpdated(balance: WalletBalance) { binding.textLayoutAmount.helperText = - "You have ${balance.availableZatoshi.convertZatoshiToZecString(8)} available" - maxZatoshi = balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI + "You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available" + maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L) } override fun onPrimaryClipChanged() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt index 986a426..f75cd51 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt @@ -13,6 +13,7 @@ import cash.z.ecc.android.feedback.Report.MetricType import cash.z.ecc.android.feedback.Report.MetricType.* import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.ui.setup.WalletSetupViewModel +import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX import cash.z.wallet.sdk.Initializer import cash.z.wallet.sdk.Synchronizer import cash.z.wallet.sdk.entity.* @@ -51,7 +52,7 @@ class SendViewModel @Inject constructor() : ViewModel() { var includeFromAddress: Boolean = false set(value) { require(!value || (value && !fromAddress.isNullOrEmpty())) { - "Error: from address was empty while attempting to include it in the memo. Verify" + + "Error: fromAddress was empty while attempting to include it in the memo. Verify" + " that initFromAddress() has previously been called on this viewmodel." } field = value @@ -60,7 +61,7 @@ class SendViewModel @Inject constructor() : ViewModel() { fun send(): Flow { funnel(SendSelected) - val memoToSend = if (includeFromAddress) "$memo\nsent from\n$fromAddress" else memo + val memoToSend = createMemoToSend() val keys = initializer.deriveSpendingKeys( lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!! ) @@ -76,6 +77,8 @@ class SendViewModel @Inject constructor() : ViewModel() { } } + fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX\n$fromAddress" else memo + private fun reportIssues(memoToSend: String) { if (toAddress == fromAddress) feedback.report(Issue.SelfSend) when { @@ -98,13 +101,16 @@ class SendViewModel @Inject constructor() : ViewModel() { when { synchronizer.validateAddress(toAddress).isNotValid -> { - emit("Please enter a valid address") + emit("Please enter a valid address.") } zatoshiAmount < 1 -> { - emit("Too little! Please enter at least 1 Zatoshi.") + emit("Please enter at least 1 Zatoshi.") } maxZatoshi != null && zatoshiAmount > maxZatoshi -> { - emit( "Too much! Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}") + emit( "Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)} ZEC.") + } + createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> { + emit( "Memo must be less than ${ZcashSdk.MAX_MEMO_SIZE} in length.") } else -> emit(null) } @@ -191,7 +197,6 @@ class SendViewModel @Inject constructor() : ViewModel() { private fun Keyed.toMetricIdFor(id: Long): String = "$id.$key" private fun String.toRelatedMetricId(): String = "$this.related" private fun String.toTxId(): Long = split('.').first().toLong() - } diff --git a/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt b/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt new file mode 100644 index 0000000..16231e7 --- /dev/null +++ b/app/src/main/java/cash/z/ecc/android/ui/util/MemoUtil.kt @@ -0,0 +1,28 @@ +package cash.z.ecc.android.ui.util + +import java.nio.charset.StandardCharsets + + +const val INCLUDE_MEMO_PREFIX = "sent from" + +inline fun ByteArray?.toUtf8Memo(): String { +// TODO: make this more official but for now, this will do + return if (this == null || this[0] >= 0xF5) "" else try { + String(this, StandardCharsets.UTF_8).trim('\u0000') + } catch (t: Throwable) { + "unable to parse memo" + } +} + + +/* + if self.0[0] < 0xF5 { + // Check if it is valid UTF8 + Some(str::from_utf8(&self.0).map(|memo| { + // Drop trailing zeroes + memo.trim_end_matches(char::from(0)).to_owned() + })) + } else { + None + } + */ \ No newline at end of file diff --git a/app/src/main/res/color/selector_feedback_button.xml b/app/src/main/res/color/selector_feedback_button.xml new file mode 100644 index 0000000..dcd144b --- /dev/null +++ b/app/src/main/res/color/selector_feedback_button.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_indicator_unknown.xml b/app/src/main/res/drawable/background_indicator_unknown.xml new file mode 100644 index 0000000..3747383 --- /dev/null +++ b/app/src/main/res/drawable/background_indicator_unknown.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feedback.xml b/app/src/main/res/layout/fragment_feedback.xml new file mode 100644 index 0000000..4a2096a --- /dev/null +++ b/app/src/main/res/layout/fragment_feedback.xml @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 6efd77f..965c89a 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -241,18 +241,6 @@ app:layout_constraintTop_toBottomOf="@id/button_number_pad_9" app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" /> - - - - - - - - - - - - - - + tools:visibility="visible" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index d81f703..6eb8400 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -65,7 +65,6 @@ android:id="@+id/nav_detail" android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment" tools:layout="@layout/fragment_detail" /> - + + diff --git a/app/src/main/res/raw/lottie_button_loading_new.json b/app/src/main/res/raw/lottie_button_loading_new.json index ccddc61..a71c5d6 100644 --- a/app/src/main/res/raw/lottie_button_loading_new.json +++ b/app/src/main/res/raw/lottie_button_loading_new.json @@ -1 +1 @@ -{"v":"5.6.0","fr":30,"ip":0,"op":200,"w":324,"h":64,"nm":"FullSync","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"StrokeFirst","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":100,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":100,"s":[15]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"t":100,"s":[990]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":100,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"StrokeLater","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[4]},{"t":174,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[0]},{"t":199,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[15]},{"t":199,"s":[100]}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":99,"op":211,"st":114,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"GoldButton","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":175,"s":[0]},{"t":186,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":201,"st":30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"DisabledButton","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[15]},{"t":140,"s":[30]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":249,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"5.6.0","fr":30,"ip":0,"op":200,"w":324,"h":64,"nm":"FullSync","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Stroke2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.5,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[4]},{"t":174,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":2,"bm":0,"nm":"Stroke 3","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[0]},{"t":192,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":99,"s":[15]},{"t":192,"s":[100]}],"ix":2},"o":{"a":0,"k":1000,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":100,"op":211,"st":99,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Stroke1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":100,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":100,"s":[15]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"t":100,"s":[990]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":100,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"GoldButton 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[320,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":12,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.725490196078,0,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":186,"s":[0]},{"t":200,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,233.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":201,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"DisabledButton","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":0,"op":249,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7ead90c..3b54d2d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,6 @@ + 0dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6267ba..f8fb643 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,7 +8,15 @@ Enter a shielded Zcash address Enter an amount to send - Your transaction is shielded and your address is not available to the recipient Your transaction is shielded but your address will be sent to the recipient via the memo + + + Any details you\'d like to share? + Was your balance clear? + What feature would you like to see next? + My experience was . . . + My balance was . . . + I\'d like . . . + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f6109ed..116bd2b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,6 +10,7 @@ @style/Zcash.ShapeAppearance.SmallComponent @style/Zcash.ShapeAppearance.MediumComponent + + + + +