Add change server ability and respond to design feedback.

- Changed style of input boxes
- Updated button behavior to only enable when values have changed
- Added simple loading screen
- Added error handling messages when the change server fails
- Switched button order and simplified button text
- Added red validation messages below input
- Respond to user input, as they type
- Reformatted title area to match other screens
- Adjusted layouts to be percentage based to work more consistently on smaller screens
- Implemented logic for restoring the original server values
This commit is contained in:
Kevin Gorham 2020-09-25 11:49:09 -04:00
parent d38626c205
commit 5e2f79ba62
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
7 changed files with 371 additions and 132 deletions

View File

@ -17,7 +17,18 @@ object Const {
object Pref { object Pref {
const val FIRST_USE_VIEW_TX = "const.pref.first_use_view_tx" const val FIRST_USE_VIEW_TX = "const.pref.first_use_view_tx"
const val FEEDBACK_ENABLED = "const.pref.feedback_enabled" const val FEEDBACK_ENABLED = "const.pref.feedback_enabled"
const val SERVER_NAME = "const.pref.server_name" const val SERVER_HOST = "const.pref.server_host"
const val SERVER_PORT = "const.pref.server_port" const val SERVER_PORT = "const.pref.server_port"
} }
/**
* Default values to use application-wide. Ideally, this set of values should remain very short.
*/
object Default {
object Server {
// If you've forked the ECC repo, change this to your hosted lightwalletd instance
const val HOST = "lightwalletd.electriccoin.co"//"your.hosted.lightwalletd.org"
const val PORT = 9067
}
}
} }

View File

@ -5,7 +5,10 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.provider.Settings import android.provider.Settings
import android.view.View
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.ui.MainActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -94,4 +97,32 @@ fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit =
throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.") throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
} }
.show() .show()
}
fun Context.showUpdateServerCriticalError(userFacingMessage: String, onConfirm: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Failed to Change Server")
.setMessage(userFacingMessage)
.setCancelable(false)
.setPositiveButton("Ok") { d, _ ->
d.dismiss()
onConfirm()
}
.show()
}
fun Context.showUpdateServerDialog(positiveText: String = "Update", onCancel: () -> Unit = {}, onUpdate: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Modify Lightwalletd Server?")
.setMessage("WARNING: Entering an invalid or untrusted server might result in misconfiguration or loss of funds!")
.setCancelable(false)
.setPositiveButton(positiveText) { dialog, _ ->
dialog.dismiss()
onUpdate()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
onCancel
}
.show()
} }

View File

@ -1,13 +1,21 @@
package cash.z.ecc.android.ui.settings package cash.z.ecc.android.ui.settings
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import cash.z.ecc.android.di.viewmodel.viewModel import android.widget.Toast
import androidx.core.widget.doAfterTextChanged
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentSettingsBinding import cash.z.ecc.android.databinding.FragmentSettingsBinding
import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.launch
class SettingsFragment : BaseFragment<FragmentSettingsBinding>() { class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
@ -16,61 +24,131 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentSettingsBinding = override fun inflate(inflater: LayoutInflater): FragmentSettingsBinding =
FragmentSettingsBinding.inflate(inflater) FragmentSettingsBinding.inflate(inflater)
//
// Lifecycle
//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
getCurrentServer() mainActivity?.preventBackPress(this)
binding.hitAreaClose.onClickNavBack() viewModel.init()
binding.buttonUpdate.setOnClickListener(View.OnClickListener { binding.apply {
validateServerHost(view) groupLoading.gone()
}) hitAreaExit.onClickNavBack()
binding.buttonReset.setOnClickListener(View.OnClickListener { buttonReset.setOnClickListener(::onResetClicked)
resetServer() buttonUpdate.setOnClickListener(::onUpdateClicked)
showUpdateServerDialog(view) buttonUpdate.isActivated = true
}) buttonReset.isActivated = true
} inputHost.doAfterTextChanged {
viewModel.pendingHost = it.toString()
private fun getCurrentServer() {
binding.inputTextLightwalletdServer.setText(viewModel.getServerHost())
binding.inputTextLightwalletdPort.setText(viewModel.getServerPort().toString())
}
private fun resetServer() {
}
private fun validateServerHost(view: View) {
var isError = false
if (binding.inputTextLightwalletdServer.text.toString().contains("http")) {
binding.lightwalletdServer.error = "Please remove http:// or https://"
isError = true
} else {
binding.lightwalletdServer.error = null
}
if (Integer.valueOf(binding.inputTextLightwalletdPort.text.toString()) > 65535) {
binding.lightwalletdPort.error = "Please enter port number below 65535"
isError = true
} else {
binding.lightwalletdPort.error = null
}
if (!isError) {
showUpdateServerDialog(view)
}
}
private fun showUpdateServerDialog(view: View) {
MaterialAlertDialogBuilder(view.context)
.setTitle("Modify lightwalletd Server?")
.setMessage("WARNING: Entering an invalid or compromised lighthttpd server might result in misconfiguration or loss of funds.")
.setCancelable(false)
.setPositiveButton("Update") { dialog, _ ->
dialog.dismiss()
updateServer()
} }
.setNegativeButton("Cancel") { dialog, _ -> inputPort.doAfterTextChanged {
dialog.dismiss() viewModel.pendingPortText = it.toString()
} }
.show() }
} }
private fun updateServer() { override fun onResume() {
super.onResume()
viewModel.uiModels.collectWith(resumedScope, ::onUiModelUpdated)
}
//
// Event handlers
//
private fun onResetClicked(unused: View?) {
mainActivity?.hideKeyboard()
context?.showUpdateServerDialog("Restore Defaults") {
resumedScope.launch {
binding.groupLoading.visible()
binding.loadingView.requestFocus()
viewModel.resetServer()
}
}
}
private fun onUpdateClicked(unused: View?) {
mainActivity?.hideKeyboard()
context?.showUpdateServerDialog {
resumedScope.launch {
binding.groupLoading.visible()
binding.loadingView.requestFocus()
viewModel.submit()
}
}
}
private fun onUiModelUpdated(uiModel: SettingsViewModel.UiModel) {
twig("onUiModelUpdated:::::$uiModel")
binding.apply {
if (handleCompletion(uiModel)) return@onUiModelUpdated
// avoid moving the cursor on instances where the change originated from the UI
if (inputHost.text.toString() != uiModel.host) inputHost.setText(uiModel.host)
if (inputPort.text.toString() != uiModel.portText) inputPort.setText(uiModel.portText)
buttonReset.isEnabled = uiModel.submitEnabled
buttonUpdate.isEnabled = uiModel.submitEnabled && !uiModel.hasError
uiModel.hostErrorMessage.let { it ->
textInputLayoutHost.helperText = it
?: R.string.settings_host_helper_text.toAppString()
textInputLayoutHost.setHelperTextColor(it.toHelperTextColor())
}
uiModel.portErrorMessage.let { it ->
textInputLayoutPort.helperText = it
?: R.string.settings_port_helper_text.toAppString()
textInputLayoutPort.setHelperTextColor(it.toHelperTextColor())
}
}
}
/**
* Handle the exit conditions and return true if we're done here.
*/
private fun handleCompletion(uiModel: SettingsViewModel.UiModel): Boolean {
return if (uiModel.changeError != null) {
binding.groupLoading.gone()
onCriticalError(uiModel.changeError)
true
} else {
if (uiModel.complete) {
binding.groupLoading.gone()
mainActivity?.safeNavigate(R.id.nav_home)
Toast.makeText(ZcashWalletApp.instance, "Successfully changed server!", Toast.LENGTH_SHORT).show()
true
}
false
}
}
private fun onCriticalError(error: Throwable) {
val details = if (error is LightWalletException.ChangeServerException.StatusException) {
error.status.description
} else {
error.javaClass.simpleName
}
val message = "An error occured while changing servers. Please verify the info" +
" and try again.\n\nError: $details"
twig(message)
Toast.makeText(ZcashWalletApp.instance, "Failed to change server!", Toast.LENGTH_SHORT).show()
context?.showUpdateServerCriticalError(message)
}
//
// Utilities
//
private fun String?.toHelperTextColor(): ColorStateList {
val color = if (this == null) {
R.color.text_light_dimmed
} else {
R.color.zcashRed
}
return ColorStateList.valueOf(color.toAppColor())
} }
} }

View File

@ -1,30 +1,99 @@
package cash.z.ecc.android.ui.settings package cash.z.ecc.android.ui.settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.module.InitializerModule import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.cancellable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named
import kotlin.properties.Delegates.observable
import kotlin.reflect.KProperty
class SettingsViewModel @Inject constructor() : ViewModel() { class SettingsViewModel @Inject constructor() : ViewModel() {
@Inject @Inject
lateinit var synchronizer: Synchronizer lateinit var synchronizer: Synchronizer
fun updateServer(host: String, port: Int) { @Inject
// TODO: Update the SecurePrefs here @Named(Const.Name.APP_PREFS)
lateinit var prefs: LockBox
lateinit var uiModels: MutableStateFlow<UiModel>
private lateinit var initialServer: UiModel
var pendingHost by observable("", ::onUpdateModel)
var pendingPortText by observable("", ::onUpdateModel)
private fun getHost(): String {
return prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
} }
fun getServerHost(): String { private fun getPort(): Int {
return InitializerModule.defaultHost return prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
} }
fun getServerPort(): Int { fun init() {
return InitializerModule.defaultPort initialServer = UiModel(getHost(), getPort().toString())
uiModels = MutableStateFlow(initialServer)
} }
override fun onCleared() { suspend fun resetServer() {
super.onCleared() UiModel(
twig("SettingsViewModel cleared!") Const.Default.Server.HOST,
Const.Default.Server.PORT.toString()
).let { default ->
uiModels.value = default
submit()
}
} }
}
suspend fun submit() {
var error: Throwable? = null
val host = uiModels.value.host
val port = uiModels.value.portInt
synchronizer.changeServer(uiModels.value.host, uiModels.value.portInt) {
error = it
}
if (error == null) {
prefs[Const.Pref.SERVER_HOST] = host
prefs[Const.Pref.SERVER_PORT] = port
}
uiModels.value = uiModels.value.copy(changeError = error, complete = true)
}
private fun onUpdateModel(kProperty: KProperty<*>, old: String, new: String) {
val pendingPort = pendingPortText.toIntOrNull() ?: -1
uiModels.value = UiModel(
pendingHost,
pendingPortText,
pendingHost != initialServer.host || pendingPortText != initialServer.portText,
if (!pendingHost.isValidHost()) "Please enter a valid host name or IP" else null,
if (pendingPort >= 65535) "Please enter a valid port number below 65535" else null
).also {
twig("updated model with $it")
}
}
data class UiModel(
val host: String = "",
val portText: String = "",
val submitEnabled: Boolean = false,
val hostErrorMessage: String? = null,
val portErrorMessage: String? = null,
val changeError: Throwable? = null,
val complete: Boolean = false
) {
val portInt get() = portText.toIntOrNull() ?: -1
val hasError get() = hostErrorMessage != null || portErrorMessage != null
}
// we can beef this up later if we want to but this is enough for now
private fun String.isValidHost(): Boolean {
return !contains("://")
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@color/text_light_dimmed" />
<item android:state_activated="false" android:color="@color/text_light_dimmed"/>
<item android:state_activated="true" android:color="@color/text_light" />
</selector>

View File

@ -1,13 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/background_home"> android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_hit_area_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<View
android:id="@+id/hit_area_exit"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:alpha="0.3"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<ImageView <ImageView
android:id="@+id/icon_profile" android:id="@+id/icon_exit"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:elevation="6dp" android:elevation="6dp"
@ -15,107 +34,128 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.912" app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064" app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08" app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" /> app:srcCompat="@drawable/ic_cancel" />
<View
android:id="@+id/hit_area_close"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:elevation="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/text_title"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_marginTop="92dp" android:autoSizeTextType="uniform"
android:maxLines="1"
android:text="@string/settings_change_lightwalletd_server" android:text="@string/settings_change_lightwalletd_server"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light" android:textColor="@color/text_light"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@id/icon_exit"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/hit_area_exit"
app:layout_constraintTop_toTopOf="@id/icon_exit" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/lightwalletd_server" android:id="@+id/text_input_layout_host"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginTop="32dp"
android:layout_marginTop="16dp" android:hint="@string/settings_server_address"
android:layout_marginEnd="24dp"
app:errorEnabled="true" app:errorEnabled="true"
app:helperText="@string/settings_host_helper_text"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" app:layout_constraintTop_toBottomOf="@+id/text_title"
tools:layout_editor_absoluteX="1dp" app:layout_constraintWidth_percent="0.84">
tools:layout_editor_absoluteY="133dp">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_text_lightwalletd_server" android:id="@+id/input_host"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/settings_server_address" /> android:background="@android:color/transparent"
android:imeOptions="actionNext"
android:maxLength="253"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/lightwalletd_port" android:id="@+id/text_input_layout_port"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginTop="18dp"
android:layout_marginTop="4dp" android:hint="@string/settings_server_port"
android:layout_marginEnd="24dp"
app:errorEnabled="true" app:errorEnabled="true"
app:helperText="@string/settings_port_helper_text"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lightwalletd_server" app:layout_constraintTop_toBottomOf="@+id/text_input_layout_host"
tools:layout_editor_absoluteX="1dp" app:layout_constraintWidth_percent="0.84">
tools:layout_editor_absoluteY="133dp">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_text_lightwalletd_port" android:id="@+id/input_port"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/settings_server_port" android:background="@android:color/transparent"
android:inputType="number" android:inputType="number"
android:maxLength="5" /> android:maxLength="5"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_update"
style="@style/Zcash.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="16dp"
android:text="@string/settings_update"
app:layout_constraintEnd_toStartOf="@+id/button_reset"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lightwalletd_port" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/button_reset" android:id="@+id/button_reset"
style="@style/Zcash.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginEnd="16dp"
android:layout_marginStart="16dp" style="@style/Zcash.Button.OutlinedButton"
android:layout_marginEnd="24dp" android:text="@string/settings_reset"
android:text="@string/settings_reset_to_default_host" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light" android:textColor="@color/selector_secondary_button_activatable"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/button_update"
app:layout_constraintStart_toEndOf="@+id/button_update" app:layout_constraintTop_toTopOf="@id/button_update"
app:layout_constraintTop_toBottomOf="@+id/lightwalletd_port" /> app:strokeColor="@color/selector_secondary_button_activatable" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
style="@style/Zcash.Button"
android:backgroundTint="@color/selector_primary_button_activatable"
android:text="@string/settings_update"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toEndOf="@id/text_input_layout_host"
app:layout_constraintTop_toBottomOf="@+id/text_input_layout_port" />
<View
android:id="@+id/loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/zcashWhite_24"
android:clickable="true"
android:focusableInTouchMode="true"
android:elevation="8dp" />
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:layout_constraintTop_toTopOf="@id/button_reset"
app:layout_constraintBottom_toBottomOf="@id/button_reset"
app:layout_constraintStart_toStartOf="@id/icon_exit"/>
<androidx.constraintlayout.widget.Group
android:id="@+id/group_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="loading_progress,loading_view" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -49,11 +49,13 @@
<string name="profile_app_version">v1.0.0-alpha05</string> <string name="profile_app_version">v1.0.0-alpha05</string>
<!-- Settings--> <!-- Settings-->
<string name="settings_change_lightwalletd_server">Change lightwalletd server:</string> <string name="settings_change_lightwalletd_server">Change Lightwalletd Server</string>
<string name="settings_server_address">Server Address</string> <string name="settings_server_address">Host</string>
<string name="settings_server_port">Server Port</string> <string name="settings_server_port">Port</string>
<string name="settings_update">Update</string> <string name="settings_update">Update</string>
<string name="settings_reset_to_default_host">Reset to Default Host</string> <string name="settings_reset">Reset</string>
<string name="settings_port_helper_text">Enter a valid port number</string>
<string name="settings_host_helper_text">Enter a valid host name or IP address</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_not_again">Don\'t show me again</string> <string name="dialog_not_again">Don\'t show me again</string>