diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..f8b7ff5 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/.kotlin/errors/errors-1778623429200.log b/.kotlin/errors/errors-1778623429200.log
new file mode 100644
index 0000000..4cf23be
--- /dev/null
+++ b/.kotlin/errors/errors-1778623429200.log
@@ -0,0 +1,77 @@
+kotlin version: 2.0.21
+error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/shihaam/git/sargit/basedbank/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt:23:5: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
+ at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
+ at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally$lambda$9$compile(IncrementalCompilerRunner.kt:249)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:267)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
+ at java.base/java.lang.reflect.Method.invoke(Unknown Source)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+ at java.base/java.lang.Thread.run(Unknown Source)
+Caused by: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
+ ... 34 more
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a9f712a..482c7fa 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,12 +5,12 @@ plugins {
android {
namespace = "sh.sar.basedbank"
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
applicationId = "sh.sar.basedbank"
minSdk = 26
- targetSdk = 35
+ targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -61,6 +61,14 @@ dependencies {
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ // QR scanning — CameraX + zxing-cpp (MIT, same stack as BinaryEye)
+ implementation("androidx.camera:camera-core:1.4.2")
+ implementation("androidx.camera:camera-camera2:1.4.2")
+ implementation("androidx.camera:camera-lifecycle:1.4.2")
+ implementation("androidx.camera:camera-view:1.4.2")
+ implementation("com.github.markusfisch:zxing-cpp:v3.0.2.5")
+
+
// Biometric authentication
implementation("androidx.biometric:biometric:1.1.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 822d33e..7ae31a6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
@@ -48,6 +49,11 @@
android:name=".ui.home.HomeActivity"
android:exported="false" />
+
+
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/QrScannerActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/QrScannerActivity.kt
new file mode 100644
index 0000000..2c7d99d
--- /dev/null
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/QrScannerActivity.kt
@@ -0,0 +1,169 @@
+package sh.sar.basedbank.ui.home
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Rect
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.Preview
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.content.ContextCompat
+import de.markusfisch.android.zxingcpp.ZxingCpp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import sh.sar.basedbank.databinding.ActivityQrScannerBinding
+import java.util.concurrent.Executors
+
+class QrScannerActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityQrScannerBinding
+ private val cameraExecutor = Executors.newSingleThreadExecutor()
+ private val parentJob = Job()
+ private val scope = CoroutineScope(Dispatchers.IO + parentJob)
+ private var resultDelivered = false
+
+ private val readerOptions = ZxingCpp.ReaderOptions(
+ tryHarder = true,
+ tryRotate = true,
+ tryInvert = true,
+ tryDownscale = true,
+ maxNumberOfSymbols = 1,
+ textMode = ZxingCpp.TextMode.PLAIN
+ )
+
+ private val permissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted -> if (granted) startCamera() else finish() }
+
+ private val pickImageLauncher = registerForActivityResult(
+ ActivityResultContracts.GetContent()
+ ) { uri ->
+ uri ?: return@registerForActivityResult
+ scope.launch {
+ val bitmap = try {
+ contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it) }
+ } catch (_: Exception) { null }
+
+ if (bitmap == null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(this@QrScannerActivity, "Could not load image", Toast.LENGTH_SHORT).show()
+ }
+ return@launch
+ }
+
+ val text = bitmap.decodeQr()?.firstOrNull()?.text
+ withContext(Dispatchers.Main) {
+ if (text != null) {
+ deliverResult(text)
+ } else {
+ Toast.makeText(this@QrScannerActivity, "No QR code found in image", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityQrScannerBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ binding.btnCancel.setOnClickListener { finish() }
+ binding.btnPickImage.setOnClickListener { pickImageLauncher.launch("image/*") }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED
+ ) startCamera()
+ else permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+
+ private fun startCamera() {
+ val future = ProcessCameraProvider.getInstance(this)
+ future.addListener({
+ val provider = try {
+ future.get()
+ } catch (_: Exception) {
+ finish(); return@addListener
+ }
+
+ val resolutionSelector = ResolutionSelector.Builder()
+ .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
+ .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
+ .build()
+
+ val preview = Preview.Builder()
+ .setResolutionSelector(resolutionSelector)
+ .build()
+ .also { it.setSurfaceProvider(binding.previewView.surfaceProvider) }
+
+ val analysis = ImageAnalysis.Builder()
+ .setResolutionSelector(resolutionSelector)
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build()
+ .also { ia ->
+ ia.setAnalyzer(cameraExecutor) { image ->
+ if (!resultDelivered) {
+ val yPlane = image.planes[0]
+ ZxingCpp.readYBuffer(
+ yPlane.buffer,
+ yPlane.rowStride,
+ Rect(0, 0, image.width, image.height),
+ image.imageInfo.rotationDegrees,
+ readerOptions
+ )?.firstOrNull()?.let { result ->
+ runOnUiThread { deliverResult(result.text) }
+ }
+ }
+ image.close()
+ }
+ }
+
+ try {
+ provider.unbindAll()
+ provider.bindToLifecycle(
+ this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
+ )
+ } catch (_: Exception) {
+ finish()
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ // Mirror BinaryEye: try LOCAL_AVERAGE first, fall back to GLOBAL_HISTOGRAM
+ private fun Bitmap.decodeQr() = ZxingCpp.readBitmap(
+ this, 0, 0, width, height, 0,
+ readerOptions.apply { binarizer = ZxingCpp.Binarizer.LOCAL_AVERAGE }
+ ) ?: ZxingCpp.readBitmap(
+ this, 0, 0, width, height, 0,
+ readerOptions.apply { binarizer = ZxingCpp.Binarizer.GLOBAL_HISTOGRAM }
+ )
+
+ private fun deliverResult(text: String) {
+ if (resultDelivered) return
+ resultDelivered = true
+ setResult(Activity.RESULT_OK, Intent().putExtra(EXTRA_QR_CONTENT, text))
+ finish()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ parentJob.cancel()
+ cameraExecutor.shutdown()
+ }
+
+ companion object {
+ const val EXTRA_QR_CONTENT = "qr_content"
+ }
+}
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
index fea0a66..8cc8f91 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt
@@ -17,6 +17,9 @@ import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
+import android.app.Activity
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContracts
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -29,6 +32,7 @@ import sh.sar.basedbank.api.mib.MibLookupException
import sh.sar.basedbank.api.mib.MibTransferClient
import sh.sar.basedbank.databinding.FragmentTransferBinding
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
+import sh.sar.basedbank.util.PaymvQrParser
import sh.sar.basedbank.util.RecentPick
import sh.sar.basedbank.util.RecentsCache
@@ -41,6 +45,19 @@ class TransferFragment : Fragment() {
private var selectedAccount: MibAccount? = null
private val session get() = (requireActivity().application as BasedBankApp).mibSession
+ private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
+ val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
+ val qr = PaymvQrParser.parse(raw)
+ if (qr == null || qr.accountNumber == null) {
+ Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
+ return@registerForActivityResult
+ }
+ if (qr.amount != null) binding.etAmount.setText(qr.amount)
+ if (qr.purpose != null) binding.etRemarks.setText(qr.purpose)
+ prefillToFromContact(qr.accountNumber, "")
+ }
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentTransferBinding.inflate(inflater, container, false)
return binding.root
@@ -61,6 +78,10 @@ class TransferFragment : Fragment() {
sheet.show(childFragmentManager, "contact_picker")
}
+ binding.btnScanQr.setOnClickListener {
+ qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
+ }
+
binding.btnTransfer.setOnClickListener {
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
@@ -90,6 +111,7 @@ class TransferFragment : Fragment() {
binding.cardToInfo.visibility = View.GONE
binding.tilTo.visibility = View.VISIBLE
binding.btnPickContact.visibility = View.VISIBLE
+ binding.btnScanQr.visibility = View.VISIBLE
binding.tilTo.error = null
}
@@ -99,6 +121,7 @@ class TransferFragment : Fragment() {
binding.cardToInfo.visibility = View.GONE
binding.tilTo.visibility = View.VISIBLE
binding.btnPickContact.visibility = View.VISIBLE
+ binding.btnScanQr.visibility = View.VISIBLE
}
}
}
@@ -147,6 +170,7 @@ class TransferFragment : Fragment() {
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
binding.tilTo.visibility = View.GONE
binding.btnPickContact.visibility = View.GONE
+ binding.btnScanQr.visibility = View.GONE
binding.cardToInfo.visibility = View.VISIBLE
saveToRecents(info)
@@ -166,6 +190,7 @@ class TransferFragment : Fragment() {
binding.cardToInfo.visibility = View.GONE
binding.tilTo.visibility = View.VISIBLE
binding.btnPickContact.visibility = View.VISIBLE
+ binding.btnScanQr.visibility = View.VISIBLE
binding.tilTo.error = null
binding.etTo.setText(accountNumber)
lookupAccount()
diff --git a/app/src/main/java/sh/sar/basedbank/util/PaymvQrParser.kt b/app/src/main/java/sh/sar/basedbank/util/PaymvQrParser.kt
new file mode 100644
index 0000000..4232a8e
--- /dev/null
+++ b/app/src/main/java/sh/sar/basedbank/util/PaymvQrParser.kt
@@ -0,0 +1,48 @@
+package sh.sar.basedbank.util
+
+data class PaymvQrData(
+ val accountNumber: String?,
+ val amount: String?,
+ val purpose: String?,
+ val merchantName: String?
+)
+
+object PaymvQrParser {
+
+ fun parse(raw: String): PaymvQrData? {
+ return try {
+ val root = parseTlv(raw)
+
+ // ID 26: Favara/PayMV merchant account info
+ val merchantInfo = root["26"]?.let { parseTlv(it) }
+
+ // ID 54: transaction amount
+ // ID 59: merchant/recipient name
+ // ID 62: additional data (sub-ID 08 = purpose)
+ val additionalData = root["62"]?.let { parseTlv(it) }
+
+ PaymvQrData(
+ accountNumber = merchantInfo?.get("03"),
+ amount = root["54"],
+ purpose = additionalData?.get("08"),
+ merchantName = root["59"]
+ )
+ } catch (_: Exception) {
+ null
+ }
+ }
+
+ private fun parseTlv(data: String): Map {
+ val result = mutableMapOf()
+ var pos = 0
+ while (pos + 4 <= data.length) {
+ val id = data.substring(pos, pos + 2)
+ val len = data.substring(pos + 2, pos + 4).toIntOrNull() ?: break
+ pos += 4
+ if (pos + len > data.length) break
+ result[id] = data.substring(pos, pos + len)
+ pos += len
+ }
+ return result
+ }
+}
diff --git a/app/src/main/res/drawable/ic_qr_scan.xml b/app/src/main/res/drawable/ic_qr_scan.xml
new file mode 100644
index 0000000..fac103a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_qr_scan.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_qr_scanner.xml b/app/src/main/res/layout/activity_qr_scanner.xml
new file mode 100644
index 0000000..97e3924
--- /dev/null
+++ b/app/src/main/res/layout/activity_qr_scanner.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_transfer.xml b/app/src/main/res/layout/fragment_transfer.xml
index f731129..e0f83b3 100644
--- a/app/src/main/res/layout/fragment_transfer.xml
+++ b/app/src/main/res/layout/fragment_transfer.xml
@@ -81,6 +81,15 @@
app:icon="@drawable/ic_contacts"
android:contentDescription="@string/transfer_pick_contact" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a09ba99..738a573 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,6 +104,9 @@
Look up account
Clear recipient
Pick contact
+ Scan QR
+ Pick image
+ Invalid or unsupported QR code
Enter an account number first
Account not found
Session unavailable — please re-login
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d10f8f9..8d1feb7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
agp = "8.9.1"
-kotlin = "2.0.21"
+kotlin = "2.1.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index cb2202e..47aad90 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = uri("https://jitpack.io") }
}
}