Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
26dcb20f7f
|
|||
|
33eb33e18c
|
|||
|
6a910facaf
|
@@ -17,6 +17,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n' | base64 -d > app/key.jks
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n' | base64 -d > app/key.jks
|
||||||
echo "${{ secrets.DOTENV_BASE64 }}" | tr -d '\n' | base64 -d > .build/release/.env
|
echo "${{ secrets.DOTENV_BASE64 }}" | tr -d '\n' | base64 -d > .build/release/.env
|
||||||
|
echo "ACCOUNT_MVR=${{ vars.ACCOUNT_MVR }}" >> .build/release/.env
|
||||||
|
echo "ACCOUNT_USD=${{ vars.ACCOUNT_USD }}" >> .build/release/.env
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
working-directory: .build/release
|
working-directory: .build/release
|
||||||
|
|||||||
+16
-2
@@ -1,8 +1,18 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val localProps = Properties().also { props ->
|
||||||
|
val f = rootProject.file("local.properties")
|
||||||
|
if (f.exists()) props.load(f.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun localOrEnv(key: String, envKey: String) =
|
||||||
|
localProps.getProperty(key) ?: System.getenv(envKey) ?: ""
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "sh.sar.basedbank"
|
namespace = "sh.sar.basedbank"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
@@ -11,10 +21,13 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 16
|
versionCode = 17
|
||||||
versionName = "1.0.17"
|
versionName = "1.0.18"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
buildConfigField("String", "ACCOUNT_MVR", "\"${localOrEnv("account.mvr", "ACCOUNT_MVR")}\"")
|
||||||
|
buildConfigField("String", "ACCOUNT_USD", "\"${localOrEnv("account.usd", "ACCOUNT_USD")}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -49,6 +62,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import sh.sar.basedbank.BuildConfig
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.databinding.FragmentSettingsAboutBinding
|
||||||
|
|
||||||
|
class SettingsAboutFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentSettingsAboutBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = FragmentSettingsAboutBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
(binding.root as? android.widget.ScrollView)?.clipToPadding = false
|
||||||
|
val basePaddingBottom = binding.root.paddingBottom
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, basePaddingBottom + if (isBottomNav) 0 else navBar.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
binding.tvAppName.text = getString(R.string.app_name)
|
||||||
|
binding.tvVersion.text = getString(R.string.about_version, BuildConfig.VERSION_NAME)
|
||||||
|
|
||||||
|
binding.rowMibTerms.setOnClickListener { openUrl("https://faisanet.mib.com.mv/terms") }
|
||||||
|
binding.rowBmlTerms.setOnClickListener { openUrl("https://www.bankofmaldives.com.mv/storage/file/121/10289/terms-conditions-online-banking-en.pdf") }
|
||||||
|
binding.rowFahipayTerms.setOnClickListener { openUrl("https://fahipay.mv/tos/") }
|
||||||
|
|
||||||
|
val hasMvr = BuildConfig.ACCOUNT_MVR.isNotEmpty()
|
||||||
|
val hasUsd = BuildConfig.ACCOUNT_USD.isNotEmpty()
|
||||||
|
|
||||||
|
if (!hasMvr && !hasUsd) {
|
||||||
|
binding.sectionDonate.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
if (!hasMvr) binding.btnDonateMvr.visibility = View.GONE
|
||||||
|
else binding.btnDonateMvr.setOnClickListener { openDonate(BuildConfig.ACCOUNT_MVR) }
|
||||||
|
if (!hasUsd) binding.btnDonateUsd.visibility = View.GONE
|
||||||
|
else binding.btnDonateUsd.setOnClickListener { openDonate(BuildConfig.ACCOUNT_USD) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String) {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDonate(accountNumber: String) {
|
||||||
|
val fragment = TransferFragment.newInstance(
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
displayName = getString(R.string.app_name),
|
||||||
|
subtitle = accountNumber,
|
||||||
|
colorHex = "#607D8B",
|
||||||
|
imageHash = null
|
||||||
|
)
|
||||||
|
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
requireActivity().title = getString(R.string.settings_about)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ class SettingsFragment : Fragment() {
|
|||||||
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance, R.string.settings_desc_appearance) { SettingsAppearanceFragment() },
|
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance, R.string.settings_desc_appearance) { SettingsAppearanceFragment() },
|
||||||
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security, R.string.settings_desc_privacy_security) { SettingsSecurityFragment() },
|
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security, R.string.settings_desc_privacy_security) { SettingsSecurityFragment() },
|
||||||
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage, R.string.settings_desc_storage) { SettingsStorageFragment() },
|
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage, R.string.settings_desc_storage) { SettingsStorageFragment() },
|
||||||
|
SettingsItem(R.drawable.ic_info, R.string.settings_about, R.string.settings_desc_about) { SettingsAboutFragment() },
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package sh.sar.basedbank.ui.login
|
package sh.sar.basedbank.ui.login
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -13,11 +14,13 @@ import android.os.Looper
|
|||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.util.OtpauthParser
|
||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
@@ -34,6 +37,7 @@ import sh.sar.basedbank.util.AccountCache
|
|||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
||||||
import sh.sar.basedbank.ui.home.HomeActivity
|
import sh.sar.basedbank.ui.home.HomeActivity
|
||||||
|
import sh.sar.basedbank.ui.home.QrScannerActivity
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
class CredentialsFragment : Fragment() {
|
class CredentialsFragment : Fragment() {
|
||||||
@@ -60,6 +64,25 @@ class CredentialsFragment : Fragment() {
|
|||||||
private var bmlLoginId: String = ""
|
private var bmlLoginId: String = ""
|
||||||
private var bmlAccumulatedAccounts = mutableListOf<BankAccount>()
|
private var bmlAccumulatedAccounts = mutableListOf<BankAccount>()
|
||||||
|
|
||||||
|
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 entries = OtpauthParser.parse(raw)
|
||||||
|
when {
|
||||||
|
entries.isEmpty() -> Toast.makeText(requireContext(), "No OTP data found in QR", Toast.LENGTH_SHORT).show()
|
||||||
|
entries.size == 1 -> binding.etOtpSeed.setText(entries[0].secret)
|
||||||
|
else -> {
|
||||||
|
val labels = entries.map { e ->
|
||||||
|
if (e.issuer.isNotBlank()) "${e.issuer} (${e.name})" else e.name.ifBlank { e.secret.take(8) + "…" }
|
||||||
|
}.toTypedArray()
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("Choose account")
|
||||||
|
.setItems(labels) { _, i -> binding.etOtpSeed.setText(entries[i].secret) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
|
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -75,7 +98,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.ivBankLogo.setImageResource(R.drawable.fahipay_logo_long)
|
binding.ivBankLogo.setImageResource(R.drawable.fahipay_logo_long)
|
||||||
binding.tvSignInDesc.setText(R.string.fahipay_sign_in_desc)
|
binding.tvSignInDesc.setText(R.string.fahipay_sign_in_desc)
|
||||||
binding.tilUsername.hint = getString(R.string.fahipay_id_card)
|
binding.tilUsername.hint = getString(R.string.fahipay_id_card)
|
||||||
binding.tilOtpSeed.visibility = android.view.View.GONE
|
binding.rowOtpSeed.visibility = android.view.View.GONE
|
||||||
binding.etOtpSeed.isEnabled = false
|
binding.etOtpSeed.isEnabled = false
|
||||||
binding.etOtpSeed.isFocusable = false
|
binding.etOtpSeed.isFocusable = false
|
||||||
}
|
}
|
||||||
@@ -83,6 +106,9 @@ class CredentialsFragment : Fragment() {
|
|||||||
|
|
||||||
binding.btnLogin.isEnabled = false
|
binding.btnLogin.isEnabled = false
|
||||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||||
|
binding.btnScanOtpSeed.setOnClickListener {
|
||||||
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
binding.cardOtp.setOnClickListener {
|
binding.cardOtp.setOnClickListener {
|
||||||
val code = binding.tvOtpCode.text.toString().replace(" ", "")
|
val code = binding.tvOtpCode.text.toString().replace(" ", "")
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Base64
|
||||||
|
|
||||||
|
data class OtpEntry(val name: String, val issuer: String, val secret: String)
|
||||||
|
|
||||||
|
object OtpauthParser {
|
||||||
|
|
||||||
|
fun parse(raw: String): List<OtpEntry> = when {
|
||||||
|
raw.startsWith("otpauth-migration://") -> parseMigration(raw)
|
||||||
|
raw.startsWith("otpauth://") -> parseStandard(raw)?.let { listOf(it) } ?: emptyList()
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStandard(raw: String): OtpEntry? {
|
||||||
|
val uri = Uri.parse(raw)
|
||||||
|
val secret = uri.getQueryParameter("secret") ?: return null
|
||||||
|
val issuer = uri.getQueryParameter("issuer") ?: ""
|
||||||
|
val label = uri.path?.trimStart('/') ?: ""
|
||||||
|
val name = if (':' in label) label.substringAfter(':').trim() else label
|
||||||
|
return OtpEntry(name, issuer, secret.uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMigration(raw: String): List<OtpEntry> {
|
||||||
|
val data = Uri.parse(raw).getQueryParameter("data") ?: return emptyList()
|
||||||
|
val bytes = try { Base64.decode(data, Base64.DEFAULT) } catch (_: Exception) { return emptyList() }
|
||||||
|
val reader = ProtobufReader(bytes)
|
||||||
|
val entries = mutableListOf<OtpEntry>()
|
||||||
|
while (reader.hasMore()) {
|
||||||
|
val tag = reader.readVarint().toInt()
|
||||||
|
val fieldNum = tag ushr 3
|
||||||
|
val wireType = tag and 0x7
|
||||||
|
if (fieldNum == 1 && wireType == 2) {
|
||||||
|
parseOtpParameters(reader.readBytes())?.let { entries.add(it) }
|
||||||
|
} else {
|
||||||
|
reader.skip(wireType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseOtpParameters(bytes: ByteArray): OtpEntry? {
|
||||||
|
val reader = ProtobufReader(bytes)
|
||||||
|
var secret: ByteArray? = null
|
||||||
|
var name = ""
|
||||||
|
var issuer = ""
|
||||||
|
var type = 2 // default to TOTP
|
||||||
|
while (reader.hasMore()) {
|
||||||
|
val tag = reader.readVarint().toInt()
|
||||||
|
val fieldNum = tag ushr 3
|
||||||
|
val wireType = tag and 0x7
|
||||||
|
when (fieldNum) {
|
||||||
|
1 -> secret = reader.readBytes()
|
||||||
|
2 -> name = String(reader.readBytes(), Charsets.UTF_8)
|
||||||
|
3 -> issuer = String(reader.readBytes(), Charsets.UTF_8)
|
||||||
|
6 -> type = reader.readVarint().toInt()
|
||||||
|
else -> reader.skip(wireType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 1) return null // skip HOTP
|
||||||
|
val secretBase32 = base32Encode(secret ?: return null)
|
||||||
|
return OtpEntry(name, issuer, secretBase32)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun base32Encode(bytes: ByteArray): String {
|
||||||
|
val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
||||||
|
val sb = StringBuilder()
|
||||||
|
var buffer = 0
|
||||||
|
var bitsLeft = 0
|
||||||
|
for (b in bytes) {
|
||||||
|
buffer = (buffer shl 8) or (b.toInt() and 0xFF)
|
||||||
|
bitsLeft += 8
|
||||||
|
while (bitsLeft >= 5) {
|
||||||
|
bitsLeft -= 5
|
||||||
|
sb.append(alphabet[(buffer ushr bitsLeft) and 0x1F])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bitsLeft > 0) sb.append(alphabet[(buffer shl (5 - bitsLeft)) and 0x1F])
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProtobufReader(private val bytes: ByteArray) {
|
||||||
|
private var pos = 0
|
||||||
|
|
||||||
|
fun hasMore() = pos < bytes.size
|
||||||
|
|
||||||
|
fun readVarint(): Long {
|
||||||
|
var result = 0L
|
||||||
|
var shift = 0
|
||||||
|
while (pos < bytes.size) {
|
||||||
|
val b = bytes[pos++].toInt() and 0xFF
|
||||||
|
result = result or ((b and 0x7F).toLong() shl shift)
|
||||||
|
if (b and 0x80 == 0) break
|
||||||
|
shift += 7
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBytes(): ByteArray {
|
||||||
|
val len = readVarint().toInt()
|
||||||
|
val data = bytes.copyOfRange(pos, pos + len)
|
||||||
|
pos += len
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skip(wireType: Int) {
|
||||||
|
when (wireType) {
|
||||||
|
0 -> readVarint()
|
||||||
|
1 -> pos += 8
|
||||||
|
2 -> readBytes()
|
||||||
|
5 -> pos += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||||
|
</vector>
|
||||||
@@ -73,22 +73,42 @@
|
|||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<LinearLayout
|
||||||
android:id="@+id/tilOtpSeed"
|
android:id="@+id/rowOtpSeed"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/otp_seed"
|
android:orientation="horizontal"
|
||||||
android:layout_marginBottom="8dp"
|
android:gravity="center_vertical"
|
||||||
app:endIconMode="password_toggle"
|
android:layout_marginBottom="8dp">
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etOtpSeed"
|
android:id="@+id/tilOtpSeed"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textPassword"
|
android:layout_weight="1"
|
||||||
android:imeOptions="actionDone"
|
android:hint="@string/otp_seed"
|
||||||
android:singleLine="true" />
|
app:endIconMode="password_toggle"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etOtpSeed"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnScanOtpSeed"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
app:icon="@drawable/ic_qr_scan"
|
||||||
|
android:contentDescription="@string/scan_otp_qr"
|
||||||
|
android:tooltipText="@string/scan_otp_qr" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/tilTotpCode"
|
android:id="@+id/tilTotpCode"
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivLogo"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:src="@drawable/ic_logo"
|
||||||
|
android:contentDescription="@string/app_name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAppName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvVersion"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:alpha="0.6"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorOutlineVariant"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/about_legal"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorOutlineVariant"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/about_terms"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowMibTerms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@drawable/mib_logo"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="MIB" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Maldives Islamic Bank"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:src="@drawable/ic_arrow_right"
|
||||||
|
android:alpha="0.4"
|
||||||
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowBmlTerms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@drawable/bml_icon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="BML" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Bank of Maldives"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:src="@drawable/ic_arrow_right"
|
||||||
|
android:alpha="0.4"
|
||||||
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowFahipayTerms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@drawable/fahipay_logo"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="Fahipay" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Fahipay"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:src="@drawable/ic_arrow_right"
|
||||||
|
android:alpha="0.4"
|
||||||
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/sectionDonate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorOutlineVariant"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/about_donate_title"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginBottom="6dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/about_donate_desc"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:alpha="0.7"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnDonateMvr"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/about_donate_mvr" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnDonateUsd"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/about_donate_usd" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
<string name="otp_seed">OTP Seed (TOTP Secret)</string>
|
<string name="otp_seed">OTP Seed (TOTP Secret)</string>
|
||||||
<string name="otp_seed_hint">The Base32 secret from your authenticator setup</string>
|
<string name="otp_seed_hint">The Base32 secret from your authenticator setup</string>
|
||||||
|
<string name="scan_otp_qr">Scan OTP QR</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
|
|
||||||
<!-- Lock screen -->
|
<!-- Lock screen -->
|
||||||
@@ -191,6 +192,16 @@
|
|||||||
<string name="settings_desc_appearance">Theme, language, and display options</string>
|
<string name="settings_desc_appearance">Theme, language, and display options</string>
|
||||||
<string name="settings_desc_privacy_security">App lock, PIN, and security preferences</string>
|
<string name="settings_desc_privacy_security">App lock, PIN, and security preferences</string>
|
||||||
<string name="settings_desc_storage">Manage cached data and storage usage</string>
|
<string name="settings_desc_storage">Manage cached data and storage usage</string>
|
||||||
|
<string name="settings_about">About</string>
|
||||||
|
<string name="settings_desc_about">App info, version, and legal</string>
|
||||||
|
<string name="about_version">Version %s</string>
|
||||||
|
<string name="about_short_desc">Thijooree is a native Android client for Maldivian banking services.</string>
|
||||||
|
<string name="about_terms">Terms of Service</string>
|
||||||
|
<string name="about_donate_title">Support Development</string>
|
||||||
|
<string name="about_donate_desc">If you find this app useful, a small donation goes a long way in keeping it alive and improving.</string>
|
||||||
|
<string name="about_donate_mvr">Donate in MVR</string>
|
||||||
|
<string name="about_donate_usd">Donate in USD</string>
|
||||||
|
<string name="about_legal">Thijooree is an independent, third-party app. It is not affiliated with, endorsed by, or officially supported by any bank or financial institution.\n\nThis app works by logging into your internet banking services directly using your credentials and communicating with bank APIs. It is built using techniques derived from reverse-engineered official bank apps. Behaviour may change or break without notice if banks update their systems.\n\nDhiraagu and Ooredoo APIs are used to look up details about phone numbers.\n\nThis app does not collect any analytics, telemetry, or personal data, and does not transmit any information about you or your usage to the developer. Everything stays entirely on your device.</string>
|
||||||
<string name="settings_logout">Log out</string>
|
<string name="settings_logout">Log out</string>
|
||||||
<string name="settings_logout_confirm_title">Log out of %s?</string>
|
<string name="settings_logout_confirm_title">Log out of %s?</string>
|
||||||
<string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string>
|
<string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user