taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit 4ec90bff4715f4961ce3c60140e6d3344bdfd3e4
parent 950535bdc6e62b2a7e816bc89e7c5bb84c0899bf
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Thu,  1 May 2025 20:46:22 +0200

[pos] adding the qr config method

Diffstat:
Mmerchant-terminal/build.gradle | 8++++++++
Mmerchant-terminal/src/main/AndroidManifest.xml | 5++++-
Mmerchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt | 23+++++++++++++++++++++--
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt | 4+++-
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt | 289++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt | 57++++++++++++++++++++++++++++-----------------------------
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt | 4++++
Mmerchant-terminal/src/main/res/layout/fragment_merchant_config.xml | 218++++++++++++++++++++++---------------------------------------------------------
Mmerchant-terminal/src/main/res/values-de/strings.xml | 2+-
Mmerchant-terminal/src/main/res/values-es/strings.xml | 2+-
Mmerchant-terminal/src/main/res/values-fr/strings.xml | 2+-
Mmerchant-terminal/src/main/res/values-tr/strings.xml | 2+-
Mmerchant-terminal/src/main/res/values-uk/strings.xml | 2+-
Mmerchant-terminal/src/main/res/values/strings.xml | 4+++-
14 files changed, 306 insertions(+), 316 deletions(-)

diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle @@ -72,6 +72,14 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.3.2" implementation "androidx.recyclerview:recyclerview-selection:1.1.0" + // CameraX + implementation "androidx.camera:camera-camera2:1.4.2" + implementation "androidx.camera:camera-lifecycle:1.4.2" + implementation "androidx.camera:camera-view:1.4.2" + + // ML Kit – on-device barcode/QR detector + implementation "com.google.mlkit:barcode-scanning:17.3.0" + // Navigation implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" diff --git a/merchant-terminal/src/main/AndroidManifest.xml b/merchant-terminal/src/main/AndroidManifest.xml @@ -18,14 +18,17 @@ xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.NFC" /> + <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.nfc.hce" android:required="false" /> - <uses-feature android:name="android.hardware.telephony" android:required="false" /> + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> <application android:allowBackup="true" diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -37,6 +37,8 @@ import com.google.android.material.navigation.NavigationView.OnNavigationItemSel import net.taler.lib.android.TalerNfcService import net.taler.merchantpos.config.Config import net.taler.merchantpos.databinding.ActivityMainBinding +import android.util.Log +import net.taler.merchantpos.config.ConfigUpdateResult class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { @@ -141,7 +143,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { * This is the URL format: * taler-pos://backend.demo.taler.net/#/username=<username>&password=<password> */ - private fun handleSetupIntent(intent: Intent) { + fun handleSetupIntent(intent: Intent) { if (intent.action != Intent.ACTION_VIEW) return val data = intent.data ?: return if (data.scheme != "taler-pos") return @@ -174,13 +176,30 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { savePassword = true ) + Log.d("MainActivity", "Config URL: $merchantUrl") + + //add check that there was no config beforehand + model.configManager.config = newConfig + // Kick off the exact same pipeline the Settings screen would start model.configManager.fetchConfig(newConfig, /*save =*/ true) - // Take the user to the fetcher fragment so they see the spinner / error handling + // Show the spinner immediately if (nav.currentDestination?.id != R.id.configFetcher) { nav.navigate(R.id.action_global_configFetcher) } + + // Observe for result + model.configManager.configUpdateResult.observe(this) { result -> + if (result is ConfigUpdateResult.Success) { + Log.d("MainActivity", "Config loaded successfully") + model.configManager.configUpdateResult.removeObservers(this) + } else if (result is ConfigUpdateResult.Error) { + Log.e("MainActivity", "Config failed: ${result.msg}") + model.configManager.configUpdateResult.removeObservers(this) + Toast.makeText(this, result.msg, Toast.LENGTH_LONG).show() + } + } } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt @@ -22,6 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT import com.google.android.material.snackbar.Snackbar import net.taler.common.navigate @@ -29,6 +30,7 @@ import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder import net.taler.merchantpos.databinding.FragmentConfigFetcherBinding +import net.taler.merchantpos.R class ConfigFetcherFragment : Fragment() { @@ -57,7 +59,7 @@ class ConfigFetcherFragment : Fragment() { null -> return@observe is ConfigUpdateResult.Error -> onNetworkError(result.msg) is ConfigUpdateResult.Success -> { - navigate(actionConfigFetcherToOrder()) + findNavController().navigate(R.id.action_global_order) } } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt @@ -16,17 +16,30 @@ package net.taler.merchantpos.config -import android.net.Uri +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle -import android.text.method.LinkMovementMethod +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn +import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import net.taler.common.navigate @@ -34,9 +47,16 @@ import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigFragmentDirections.Companion.actionSettingsToOrder import net.taler.merchantpos.databinding.FragmentMerchantConfigBinding +import androidx.core.view.isVisible +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import net.taler.merchantpos.MainActivity /** - * Fragment that displays merchant settings. + * Fragment that displays merchant settings, either by scanning a QR code + * or by manual token entry. */ class ConfigFragment : Fragment() { @@ -45,88 +65,66 @@ class ConfigFragment : Fragment() { private lateinit var ui: FragmentMerchantConfigBinding + private val scanner by lazy { + BarcodeScanning.getClient( + BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + ) + } + + private val cameraExecutor by lazy { + ContextCompat.getMainExecutor(requireContext()) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { ui = FragmentMerchantConfigBinding.inflate(inflater, container, false) return ui.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - ui.configToggle.check(when (configManager.config) { - is Config.Old -> R.id.oldConfigButton - is Config.New -> R.id.newConfigButton - }) + // set initial toggle + ui.configToggle.check(R.id.newConfigButton) - ui.oldConfigButton.setOnClickListener { - showOldConfig() - } + // wire up toggle group for QR vs manual + ui.configToggle.addOnButtonCheckedListener { _: MaterialButtonToggleGroup, checkedId: Int, isChecked: Boolean -> + if (!isChecked) return@addOnButtonCheckedListener - ui.newConfigButton.setOnClickListener { - showNewConfig() - } - - /* - * Old configuration (JSON) - */ - - ui.configUrlView.editText!!.setOnFocusChangeListener { _, hasFocus -> - if (!hasFocus) checkForUrlCredentials() - } - - ui.okOldButton.setOnClickListener { - checkForUrlCredentials() - val inputUrl = ui.configUrlView.editText!!.text - val url = if (inputUrl.startsWith("http")) { - inputUrl.toString() - } else { - "https://$inputUrl".also { ui.configUrlView.editText!!.setText(it) } - } - // ui.progressBarOld.visibility = VISIBLE - ui.okOldButton.visibility = INVISIBLE - val config = Config.Old( - configUrl = url, - username = ui.usernameView.editText!!.text.toString(), - password = ui.passwordView.editText!!.text.toString(), - savePassword = ui.savePasswordCheckBox.isChecked, - ) - configManager.fetchConfig(config, true) - configManager.configUpdateResult.observe(viewLifecycleOwner) { result -> - if (onConfigUpdate(result)) { - configManager.configUpdateResult.removeObservers(viewLifecycleOwner) - } + when (checkedId) { + R.id.qrConfigButton -> showQrConfig() + R.id.newConfigButton -> showManualConfig() } } - ui.forgetPasswordButton.setOnClickListener { - configManager.forgetPassword() - ui.passwordView.editText!!.text = null - ui.forgetPasswordButton.visibility = GONE - } - - ui.configDocsView.movementMethod = LinkMovementMethod.getInstance() - - /* - * New configuration (Merchant) - */ +// configManager.configUpdateResult +// .observe(viewLifecycleOwner) { result -> +// if (result != null && onConfigUpdate(result)) { +// // one‐shot observer +// configManager.configUpdateResult.removeObservers(viewLifecycleOwner) +// } +// } + // manual configuration OK button ui.okNewButton.setOnClickListener { - val inputUrl = ui.merchantUrlView.editText!!.text + val inputUrl = ui.merchantUrlView.editText!!.text.toString() val url = if (inputUrl.startsWith("http")) { - inputUrl.toString() + inputUrl } else { "https://$inputUrl".also { ui.merchantUrlView.editText!!.setText(it) } } - // ui.progressBarNew.visibility = VISIBLE + ui.progressBarNew.visibility = VISIBLE ui.okNewButton.visibility = INVISIBLE val config = Config.New( merchantUrl = url, accessToken = ui.tokenView.editText!!.text.toString(), savePassword = ui.saveTokenCheckBox.isChecked, ) + configManager.fetchConfig(config, true) configManager.configUpdateResult.observe(viewLifecycleOwner) { result -> if (onConfigUpdate(result)) { @@ -140,85 +138,58 @@ class ConfigFragment : Fragment() { override fun onStart() { super.onStart() - // focus password if this is the only empty field - if (ui.passwordView.editText!!.text.isBlank() - && ui.configUrlView.editText!!.text.isNotBlank() - && ui.usernameView.editText!!.text.isNotBlank() - ) { - ui.passwordView.requestFocus() - } + // nothing to do here } - private fun updateView(isInitialization: Boolean = false) { - if (isInitialization) { - ui.configUrlView.editText!!.setText(OLD_CONFIG_URL_DEMO) - ui.usernameView.editText!!.setText(OLD_CONFIG_USERNAME_DEMO) - ui.passwordView.editText!!.setText(OLD_CONFIG_PASSWORD_DEMO) + override fun onResume() { + super.onResume() + // if QR form is showing, re-request camera + if (ui.qrConfigForm.isVisible) { + requestCameraIfNeeded() + } + } - ui.merchantUrlView.editText!!.setText(NEW_CONFIG_URL_DEMO) + override fun onDestroyView() { + // ensure camera is released + stopCamera() + super.onDestroyView() + } - when (val config = configManager.config) { - is Config.Old -> { - if (config.configUrl.isNotBlank()) { - ui.configUrlView.editText!!.setText(config.configUrl) - } + private fun showQrConfig() { + Log.d("ConfigFragment", "showQrConfig() → requesting camera") + ui.qrConfigForm.visibility = VISIBLE + ui.newConfigForm.visibility = GONE + requestCameraIfNeeded() + } - if (config.username.isNotBlank()) { - ui.usernameView.editText!!.setText(config.username) - } + private fun showManualConfig() { + ui.qrConfigForm.visibility = GONE + ui.newConfigForm.visibility = VISIBLE + stopCamera() + } - ui.savePasswordCheckBox.isChecked = config.savePassword - } + private fun updateView(isInitialization: Boolean = false) { + if (isInitialization) { + ui.merchantUrlView.editText!!.setText(NEW_CONFIG_URL_DEMO) + when (val cfg = configManager.config) { is Config.New -> { - if (config.merchantUrl.isNotBlank()) { - ui.merchantUrlView.editText!!.setText(config.merchantUrl) + if (cfg.merchantUrl.isNotBlank()) { + ui.merchantUrlView.editText!!.setText(cfg.merchantUrl) } - - ui.saveTokenCheckBox.isChecked = config.savePassword + ui.saveTokenCheckBox.isChecked = cfg.savePassword } } } when (configManager.config) { - is Config.Old -> { - ui.configToggle.check(R.id.oldConfigButton) - showOldConfig() - } is Config.New -> { ui.configToggle.check(R.id.newConfigButton) - showNewConfig() + showManualConfig() } } - } - private fun showOldConfig() { - ui.oldConfigForm.visibility = VISIBLE - ui.newConfigForm.visibility = GONE - } - - private fun showNewConfig() { - ui.oldConfigForm.visibility = GONE - ui.newConfigForm.visibility = VISIBLE - } - - private fun checkForUrlCredentials() { - val text = ui.configUrlView.editText!!.text.toString() - Uri.parse(text)?.userInfo?.let { userInfo -> - if (userInfo.contains(':')) { - val (user, pass) = userInfo.split(':') - val strippedUrl = text.replace("${userInfo}@", "") - ui.configUrlView.editText!!.setText(strippedUrl) - ui.usernameView.editText!!.setText(user) - ui.passwordView.editText!!.setText(pass) - } - } - } - - /** - * Processes updated config and returns true, if observer can be removed. - */ private fun onConfigUpdate(result: ConfigUpdateResult?) = when (result) { null -> false is ConfigUpdateResult.Error -> { @@ -244,10 +215,90 @@ class ConfigFragment : Fragment() { } private fun onResultReceived() { - ui.progressBarOld.visibility = INVISIBLE - ui.okOldButton.visibility = VISIBLE ui.progressBarNew.visibility = INVISIBLE ui.okNewButton.visibility = VISIBLE } + // ─── CameraX integration ─────────────────────────────────────────── + + // 1) permission launcher + private val requestCameraPerm = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + Log.d("ConfigFragment", "CAMERA permission granted? $granted") + if (granted) startCamera() + else Toast.makeText(requireContext(), + "Camera permission is required for QR scanning", Toast.LENGTH_SHORT).show() + } + + // 2) request if needed + private fun requestCameraIfNeeded() { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + startCamera() + } else { + requestCameraPerm.launch(Manifest.permission.CAMERA) + } + } + + // 3) start CameraX preview + @OptIn(ExperimentalGetImage::class) + private fun startCamera() { + Log.d("ConfigFragment", "startCamera() called") + val providerFuture = ProcessCameraProvider.getInstance(requireContext()) + providerFuture.addListener({ + val provider = providerFuture.get() + + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(ui.previewView.surfaceProvider) + } + + val analysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build().also { useCase -> + useCase.setAnalyzer(cameraExecutor) { proxy -> + val media = proxy.image ?: run { proxy.close(); return@setAnalyzer } + val image = InputImage.fromMediaImage( + media, + proxy.imageInfo.rotationDegrees + ) + scanner.process(image) + .addOnSuccessListener { codes -> + codes.firstOrNull()?.rawValue?.let { onQrDecoded(it) } + } + .addOnCompleteListener { proxy.close() } + } + } + + provider.unbindAll() + provider.bindToLifecycle( + viewLifecycleOwner, + CameraSelector.DEFAULT_BACK_CAMERA, + preview, + analysis + ) + }, cameraExecutor) + } + + private fun onQrDecoded(raw: String) { + if (!raw.startsWith("taler-pos://")) return // guard + + stopCamera() // freeze picture + // Re-use the rock-solid parsing inside MainActivity + val intent = Intent(Intent.ACTION_VIEW, raw.toUri()) + (requireActivity() as MainActivity).handleSetupIntent(intent) + + // show loader until ConfigFetcherFragment takes over + ui.progressBarQr.visibility = View.VISIBLE + ui.previewView.visibility = View.INVISIBLE + } + + // 4) release camera + private fun stopCamera() { + try { + ProcessCameraProvider.getInstance(requireContext()) + .get() + .unbindAll() + } catch (_: Exception) { /* no-op */ } + } } + diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt @@ -43,6 +43,7 @@ import net.taler.merchantlib.MerchantApi import net.taler.merchantlib.MerchantConfig import net.taler.merchantpos.BuildConfig import net.taler.merchantpos.R +import androidx.core.net.toUri private const val SETTINGS_NAME = "taler-merchant-terminal" @@ -67,7 +68,7 @@ internal const val OLD_CONFIG_PASSWORD_DEMO = "" private const val SETTINGS_MERCHANT_URL = "merchantUrl" private const val SETTINGS_ACCESS_TOKEN = "accessToken" -internal const val NEW_CONFIG_URL_DEMO = "https://backend.demo.taler.net/instances/pos" +internal const val NEW_CONFIG_URL_DEMO = "https://backend.demo.taler.net/instances/sandbox" private val VERSION = Version.parse(BuildConfig.BACKEND_API_VERSION)!! @@ -84,26 +85,22 @@ class ConfigManager( private val context: Context, private val scope: CoroutineScope, private val httpClient: HttpClient, - private val api: MerchantApi + private val api: MerchantApi, ) { private val prefs = context.getSharedPreferences(SETTINGS_NAME, MODE_PRIVATE) private val configurationReceivers = ArrayList<ConfigurationReceiver>() - var config: Config = if (prefs.getInt(SETTINGS_CONFIG_VERSION, CONFIG_VERSION_NEW) == CONFIG_VERSION_NEW) { + init { + migrateLegacyPrefsIfNeeded(); + } + + var config: Config = Config.New( merchantUrl = prefs.getString(SETTINGS_MERCHANT_URL, "")!!, accessToken = prefs.getString(SETTINGS_ACCESS_TOKEN, "")!!, savePassword = prefs.getBoolean(SETTINGS_SAVE_PASSWORD, true), ) - } else { - Config.Old( - configUrl = prefs.getString(SETTINGS_CONFIG_URL, "")!!, - username = prefs.getString(SETTINGS_USERNAME, OLD_CONFIG_USERNAME_DEMO)!!, - password = prefs.getString(SETTINGS_PASSWORD, OLD_CONFIG_PASSWORD_DEMO)!!, - savePassword = prefs.getBoolean(SETTINGS_SAVE_PASSWORD, true) - ) - } @Volatile var merchantConfig: MerchantConfig? = null @@ -120,6 +117,13 @@ class ConfigManager( configurationReceivers.add(receiver) } + private fun migrateLegacyPrefsIfNeeded() { + val legacyVersion = prefs.getInt(SETTINGS_CONFIG_VERSION, CONFIG_VERSION_NEW) + if (legacyVersion == CONFIG_VERSION_OLD) { + prefs.edit().clear().apply() + } + } + @UiThread fun reloadConfig() { fetchConfig(config, true) @@ -130,7 +134,7 @@ class ConfigManager( mConfigUpdateResult.value = null val configToSave = if (save) { if (config.savePassword()) config else when (val c = config) { - is Config.Old -> c.copy(password = "") + //is Config.Old -> c.copy(password = "") is Config.New -> c.copy(accessToken = "") } } else null @@ -138,8 +142,8 @@ class ConfigManager( scope.launch(Dispatchers.IO) { try { val url = when(val c = config) { - is Config.Old -> c.configUrl - is Config.New -> Uri.parse(c.merchantUrl) + //is Config.Old -> c.configUrl + is Config.New -> c.merchantUrl.toUri() .buildUpon() .appendPath("private/pos") .build() @@ -149,11 +153,6 @@ class ConfigManager( // get PoS configuration val posConfig: PosConfig = httpClient.get(url) { when (val c = config) { - is Config.Old -> { - val credentials = "${c.username}:${c.password}" - val auth = ("Basic ${encodeToString(credentials.toByteArray(), NO_WRAP)}") - header(Authorization, auth) - } is Config.New -> { val token = "secret-token:${c.accessToken}" val auth = ("Bearer $token") @@ -163,7 +162,7 @@ class ConfigManager( }.body() val merchantConfig = when (val c = config) { - is Config.Old -> posConfig.merchantConfig!! + //is Config.Old -> posConfig.merchantConfig!! is Config.New -> MerchantConfig(c.merchantUrl, "secret-token:${c.accessToken}") } @@ -191,7 +190,7 @@ class ConfigManager( newConfig: Config?, posConfig: PosConfig, merchantConfig: MerchantConfig, - configResponse: ConfigResponse + configResponse: ConfigResponse, ) { val versionIncompatible = VERSION.getIncompatibleStringOrNull(context, configResponse.version) @@ -224,7 +223,7 @@ class ConfigManager( @UiThread fun forgetPassword() { config = when (val c = config) { - is Config.Old -> c.copy(password = "") + //is Config.Old -> c.copy(password = "") is Config.New -> c.copy(accessToken = "") } saveConfig(config) @@ -234,13 +233,13 @@ class ConfigManager( @UiThread private fun saveConfig(config: Config) { when (val c = config) { - is Config.Old -> prefs.edit() - .putInt(SETTINGS_CONFIG_VERSION, CONFIG_VERSION_OLD) - .putString(SETTINGS_CONFIG_URL, c.configUrl) - .putString(SETTINGS_USERNAME, c.username) - .putString(SETTINGS_PASSWORD, c.password) - .putBoolean(SETTINGS_SAVE_PASSWORD, c.savePassword) - .apply() +// is Config.Old -> prefs.edit() +// .putInt(SETTINGS_CONFIG_VERSION, CONFIG_VERSION_OLD) +// .putString(SETTINGS_CONFIG_URL, c.configUrl) +// .putString(SETTINGS_USERNAME, c.username) +// .putString(SETTINGS_PASSWORD, c.password) +// .putBoolean(SETTINGS_SAVE_PASSWORD, c.savePassword) +// .apply() is Config.New -> prefs.edit() .putInt(SETTINGS_CONFIG_VERSION, CONFIG_VERSION_NEW) .putString(SETTINGS_MERCHANT_URL, c.merchantUrl) diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt @@ -34,7 +34,10 @@ sealed class Config { /** * JSON config URL + user/password + * + * @Deprecated("Use New instead") */ + /* data class Old( val configUrl: String, val username: String, @@ -45,6 +48,7 @@ sealed class Config { override fun hasPassword() = password.isNotBlank() override fun savePassword() = savePassword } + */ /** * Merchant URL + access token diff --git a/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml b/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml @@ -16,7 +16,6 @@ <ScrollView 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:fillViewport="true"> @@ -24,222 +23,128 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - tools:context=".config.ConfigFragment"> + android:orientation="vertical"> + <!-- ─── 1) mode toggle ─────────────────────────────────────────────── --> <com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/configToggle" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleSelection="true" app:checkedButton="@id/newConfigButton"> + <Button style="?attr/materialButtonOutlinedStyle" - android:id="@+id/oldConfigButton" + android:id="@+id/qrConfigButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/config_old_label"/> + android:text="@string/config_qr_label" /> + <Button style="?attr/materialButtonOutlinedStyle" android:id="@+id/newConfigButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/config_new_label" /> + android:text="@string/config_setup_password" /> </com.google.android.material.button.MaterialButtonToggleGroup> + <!-- ─── 2) QR-scanner form ──────────────────────────────── --> <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/oldConfigForm" + android:id="@+id/qrConfigForm" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" - tools:visibility="visible"> + android:padding="24dp" + android:visibility="gone"> - <androidx.cardview.widget.CardView - android:id="@+id/deprecationCard" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:contentPadding="10dp" - android:layout_margin="16dp" - app:cardBackgroundColor="@color/red" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent"> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAlignment="center" - android:textColor="@android:color/white" - android:text="@string/config_old_deprecation" /> - </androidx.cardview.widget.CardView> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/configUrlView" + <!-- CameraX preview – now 50 % width, 60 % height --> + <androidx.camera.view.PreviewView + android:id="@+id/previewView" android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:hint="@string/config_url" - app:boxBackgroundColor="@android:color/transparent" - app:boxBackgroundMode="outline" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/deprecationCard"> + android:layout_height="0dp" + android:scaleType="fitCenter" - <com.google.android.material.textfield.TextInputEditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textUri" /> - - </com.google.android.material.textfield.TextInputLayout> + app:layout_constraintWidth_percent="0.5" + app:layout_constraintHeight_percent="0.8" - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/usernameView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:hint="@string/config_username" - app:boxBackgroundColor="@android:color/transparent" - app:boxBackgroundMode="outline" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/configUrlView"> - - <com.google.android.material.textfield.TextInputEditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="text" /> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/passwordView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:hint="@string/config_password" - app:boxBackgroundColor="@android:color/transparent" - app:boxBackgroundMode="outline" - app:endIconMode="password_toggle" - app:layout_constraintEnd_toStartOf="@+id/forgetPasswordButton" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/usernameView"> - - <com.google.android.material.textfield.TextInputEditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textWebPassword" /> - - </com.google.android.material.textfield.TextInputLayout> - - <Button - android:id="@+id/forgetPasswordButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/config_forget_password" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/passwordView" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/passwordView" - tools:visibility="visible" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> - <CheckBox - android:id="@+id/savePasswordCheckBox" - android:layout_width="0dp" + <!-- help / status text --> + <TextView + android:id="@+id/hintView" + style="@style/TextAppearance.Material3.BodyMedium" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" - android:checked="true" - android:text="@string/config_save_password" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/okOldButton" - app:layout_constraintHorizontal_chainStyle="spread_inside" + android:text="@string/scan_qr_hint" + app:layout_constraintTop_toBottomOf="@id/previewView" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/passwordView" - app:layout_constraintVertical_bias="0.0" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/okOldButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/config_ok" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/savePasswordCheckBox" - app:layout_constraintTop_toBottomOf="@+id/passwordView" - app:layout_constraintVertical_bias="0.0" /> + app:layout_constraintEnd_toEndOf="parent" /> + <!-- progress spinner --> <ProgressBar - android:id="@+id/progressBarOld" - style="?android:attr/progressBarStyle" + android:id="@+id/progressBarQr" + style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="@+id/okOldButton" - app:layout_constraintEnd_toEndOf="@+id/okOldButton" - app:layout_constraintStart_toStartOf="@+id/okOldButton" - app:layout_constraintTop_toTopOf="@+id/okOldButton" - tools:visibility="visible" /> - - <TextView - android:id="@+id/configDocsView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/config_docs" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/okOldButton" /> + app:layout_constraintTop_toTopOf="@id/previewView" + app:layout_constraintBottom_toBottomOf="@id/previewView" + app:layout_constraintStart_toStartOf="@id/previewView" + app:layout_constraintEnd_toEndOf="@id/previewView" /> </androidx.constraintlayout.widget.ConstraintLayout> + <!-- ─── 3) Manual-token form (unchanged) ──────────────────────────── --> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/newConfigForm" android:layout_width="match_parent" android:layout_height="wrap_content"> + <!-- Merchant-URL field --> <com.google.android.material.textfield.TextInputLayout android:id="@+id/merchantUrlView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="16dp" android:hint="@string/config_merchant_url" - app:boxBackgroundColor="@android:color/transparent" app:boxBackgroundMode="outline" - app:layout_constraintEnd_toEndOf="parent" + app:boxBackgroundColor="@android:color/transparent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textUri" /> - </com.google.android.material.textfield.TextInputLayout> + <!-- Access-token field --> <com.google.android.material.textfield.TextInputLayout android:id="@+id/tokenView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="16dp" android:hint="@string/config_token" - app:boxBackgroundColor="@android:color/transparent" app:boxBackgroundMode="outline" + app:boxBackgroundColor="@android:color/transparent" app:endIconMode="password_toggle" - app:layout_constraintEnd_toStartOf="@+id/forgetTokenButton" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/merchantUrlView"> + app:layout_constraintEnd_toStartOf="@+id/forgetTokenButton" + app:layout_constraintTop_toBottomOf="@id/merchantUrlView"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textWebPassword" /> - </com.google.android.material.textfield.TextInputLayout> + <!-- “Forget” token button --> <Button android:id="@+id/forgetTokenButton" android:layout_width="wrap_content" @@ -247,11 +152,11 @@ android:layout_margin="16dp" android:text="@string/config_forget_password" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/tokenView" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/tokenView" - tools:visibility="visible" /> + app:layout_constraintTop_toTopOf="@id/tokenView" + app:layout_constraintBottom_toBottomOf="@id/tokenView" + app:layout_constraintEnd_toEndOf="parent" /> + <!-- save-password checkbox --> <CheckBox android:id="@+id/saveTokenCheckBox" android:layout_width="0dp" @@ -261,39 +166,36 @@ android:layout_marginBottom="16dp" android:checked="true" android:text="@string/config_save_password" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/okNewButton" - app:layout_constraintHorizontal_chainStyle="spread_inside" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/tokenView" - app:layout_constraintVertical_bias="0.0" /> + app:layout_constraintEnd_toStartOf="@+id/okNewButton" + app:layout_constraintTop_toBottomOf="@id/tokenView" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_chainStyle="spread_inside" /> + <!-- “OK” button --> <com.google.android.material.button.MaterialButton android:id="@+id/okNewButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:text="@string/config_ok" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/saveTokenCheckBox" - app:layout_constraintTop_toBottomOf="@+id/tokenView" - app:layout_constraintVertical_bias="0.0" /> + app:layout_constraintTop_toBottomOf="@id/tokenView" + app:layout_constraintBottom_toBottomOf="parent" /> + <!-- progress spinner --> <ProgressBar android:id="@+id/progressBarNew" style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="@+id/okNewButton" - app:layout_constraintEnd_toEndOf="@+id/okNewButton" - app:layout_constraintStart_toStartOf="@+id/okNewButton" - app:layout_constraintTop_toTopOf="@+id/okNewButton" - tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="@id/okNewButton" + app:layout_constraintEnd_toEndOf="@id/okNewButton" + app:layout_constraintTop_toTopOf="@id/okNewButton" + app:layout_constraintBottom_toBottomOf="@id/okNewButton" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> - </ScrollView> diff --git a/merchant-terminal/src/main/res/values-de/strings.xml b/merchant-terminal/src/main/res/values-de/strings.xml @@ -68,7 +68,7 @@ <string name="order_custom_product_default">Tipp</string> <string name="order_custom_add_button">Hinzufügen</string> <string name="config_old_label">JSON-Datei (alt)</string> - <string name="config_new_label">Händler (neu)</string> + <string name="config_setup_password">Händler (neu)</string> <string name="config_merchant_url">Händler-URL</string> <string name="config_token">Zugangstoken</string> <string name="order_custom">Benutzerdefinierten Artikel hinzufügen</string> diff --git a/merchant-terminal/src/main/res/values-es/strings.xml b/merchant-terminal/src/main/res/values-es/strings.xml @@ -72,7 +72,7 @@ <string name="config_old_label">Archivo JSON (antiguo)</string> <string name="config_old_deprecation">Este método de configuración está obsoleto, por favor usa la nueva configuración API para vendedor.</string> <string name="config_merchant_url">URL de vendedor</string> - <string name="config_new_label">Vendedor (nuevo)</string> + <string name="config_setup_password">Vendedor (nuevo)</string> <string name="config_token">Token de acceso</string> <string name="host_apdu_service_desc">Pagos NFC en Taler Merchant</string> </resources> \ No newline at end of file diff --git a/merchant-terminal/src/main/res/values-fr/strings.xml b/merchant-terminal/src/main/res/values-fr/strings.xml @@ -67,7 +67,7 @@ <string name="error_timeout">Aucun paiement n\'a été effectué pendant la période de paiement, veuillez réessayer !</string> <string name="product_image">Image du produit</string> <string name="order_custom_add_button">Ajouter</string> - <string name="config_new_label">Commerçant (nouveau)</string> + <string name="config_setup_password">Commerçant (nouveau)</string> <string name="product_category_uncategorized">Sans catégorie</string> <string name="config_old_label">Fichier JSON (ancien)</string> <string name="config_merchant_url">URL du commerçant</string> diff --git a/merchant-terminal/src/main/res/values-tr/strings.xml b/merchant-terminal/src/main/res/values-tr/strings.xml @@ -72,7 +72,7 @@ <string name="menu_reload">Yeniden yükle</string> <string name="toast_reloading">Envanter yeniden yükleniyor</string> <string name="config_old_label">JSON dosyası (eski)</string> - <string name="config_new_label">Satıcı (yeni)</string> + <string name="config_setup_password">Satıcı (yeni)</string> <string name="config_merchant_url">Satıcı URL\'si</string> <string name="config_token">Erişim jetonu</string> <string name="config_old_deprecation">Bu yapılandırma yöntemi kullanımdan kaldırıldı. Lütfen yeni satıcı API yapılandırmasını kullanın.</string> diff --git a/merchant-terminal/src/main/res/values-uk/strings.xml b/merchant-terminal/src/main/res/values-uk/strings.xml @@ -18,7 +18,7 @@ <string name="order_custom_product_default">Чайові</string> <string name="order_custom_add_button">Додати</string> <string name="config_old_label">JSON файл (старий)</string> - <string name="config_new_label">Продавець (новий)</string> + <string name="config_setup_password">Продавець (новий)</string> <string name="config_url">URL конфігурації</string> <string name="config_password">Пароль</string> <string name="config_merchant_url">URL продавця</string> diff --git a/merchant-terminal/src/main/res/values/strings.xml b/merchant-terminal/src/main/res/values/strings.xml @@ -26,7 +26,7 @@ <string name="config_label">Merchant settings</string> <string name="config_old_label">JSON file (old)</string> - <string name="config_new_label">Merchant (new)</string> + <string name="config_setup_password">Password</string> <string name="config_old_deprecation">This configuration method is deprecated, please use the new merchant API configuration.</string> <string name="config_url">Configuration URL</string> <string name="config_merchant_url">Merchant URL</string> @@ -87,4 +87,6 @@ <string name="toast_back_to_exit">Click «back» again to exit</string> <string name="host_apdu_service_desc">Taler Merchant NFC payments</string> + <string name="config_qr_label">QR config</string> + <string name="scan_qr_hint">Scan the QR code from Merchant Backoffice</string> </resources>