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") } } }