Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
26dcb20f7f
|
|||
|
33eb33e18c
|
|||
|
6a910facaf
|
|||
|
e3c6b3a695
|
@@ -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 = 15
|
versionCode = 17
|
||||||
versionName = "1.0.16"
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import sh.sar.basedbank.api.mib.MibCard
|
|||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
|
import sh.sar.basedbank.util.NfcPaymentUtil
|
||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
@@ -426,11 +427,13 @@ class DashboardFragment : Fragment() {
|
|||||||
if (isMib) {
|
if (isMib) {
|
||||||
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
val accountNumber = (item as CardItem.Bml).account.accountNumber
|
NfcPaymentUtil.checkAndProceed(requireContext()) {
|
||||||
(requireActivity() as HomeActivity).navigateTo(
|
val accountNumber = (item as CardItem.Bml).account.accountNumber
|
||||||
R.id.nav_pay_with_card,
|
(requireActivity() as HomeActivity).navigateTo(
|
||||||
CardsFragment.newInstanceWithAutoTapMode(accountNumber)
|
R.id.nav_pay_with_card,
|
||||||
)
|
CardsFragment.newInstanceWithAutoTapMode(accountNumber)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import sh.sar.basedbank.util.CardsCache
|
|||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.Totp
|
import sh.sar.basedbank.util.Totp
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
|
import sh.sar.basedbank.util.NfcPaymentUtil
|
||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@@ -232,11 +233,13 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
val bmlItem = item as CardItem.Bml
|
val bmlItem = item as CardItem.Bml
|
||||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
NfcPaymentUtil.checkAndProceed(requireContext()) {
|
||||||
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
showBiometricPromptForTap(bmlItem)
|
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
|
||||||
} else {
|
showBiometricPromptForTap(bmlItem)
|
||||||
setTapMode(true, bmlItem)
|
} else {
|
||||||
|
setTapMode(true, bmlItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,11 +745,13 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
currentCardPosition = pos
|
currentCardPosition = pos
|
||||||
binding.rvCards.scrollToPosition(pos)
|
binding.rvCards.scrollToPosition(pos)
|
||||||
}
|
}
|
||||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
NfcPaymentUtil.checkAndProceed(requireContext()) {
|
||||||
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
showBiometricPromptForTap(targetCard)
|
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
|
||||||
} else {
|
showBiometricPromptForTap(targetCard)
|
||||||
setTapMode(true, targetCard)
|
} else {
|
||||||
|
setTapMode(true, targetCard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,53 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.nfc.NfcAdapter
|
||||||
|
import android.nfc.cardemulation.CardEmulation
|
||||||
|
import android.provider.Settings
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.nfc.BmlHostCardEmulatorService
|
||||||
|
|
||||||
|
object NfcPaymentUtil {
|
||||||
|
fun checkAndProceed(context: Context, onReady: () -> Unit) {
|
||||||
|
val nfcAdapter = NfcAdapter.getDefaultAdapter(context) ?: run {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.nfc_unsupported_title)
|
||||||
|
.setMessage(R.string.nfc_unsupported_message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nfcAdapter.isEnabled) {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.nfc_disabled_title)
|
||||||
|
.setMessage(R.string.nfc_disabled_message)
|
||||||
|
.setPositiveButton(R.string.nfc_open_settings) { _, _ ->
|
||||||
|
context.startActivity(Intent(Settings.ACTION_NFC_SETTINGS))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardEmulation = CardEmulation.getInstance(nfcAdapter)
|
||||||
|
val componentName = ComponentName(context, BmlHostCardEmulatorService::class.java)
|
||||||
|
if (!cardEmulation.isDefaultServiceForCategory(componentName, CardEmulation.CATEGORY_PAYMENT)) {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.nfc_not_default_title)
|
||||||
|
.setMessage(context.getString(R.string.nfc_not_default_message,
|
||||||
|
context.applicationInfo.loadLabel(context.packageManager)))
|
||||||
|
.setPositiveButton(R.string.nfc_payment_open_settings) { _, _ ->
|
||||||
|
context.startActivity(Intent(Settings.ACTION_NFC_PAYMENT_SETTINGS))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -334,6 +345,14 @@
|
|||||||
<string name="card_pay_qr">Scan to Pay</string>
|
<string name="card_pay_qr">Scan to Pay</string>
|
||||||
<string name="card_pay_nfc">Tap to Pay</string>
|
<string name="card_pay_nfc">Tap to Pay</string>
|
||||||
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
|
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
|
||||||
|
<string name="nfc_unsupported_title">Not Supported</string>
|
||||||
|
<string name="nfc_unsupported_message">Tap to Pay is not supported on this device.</string>
|
||||||
|
<string name="nfc_disabled_title">NFC is Off</string>
|
||||||
|
<string name="nfc_disabled_message">Turn on NFC to use Tap to Pay.</string>
|
||||||
|
<string name="nfc_open_settings">NFC Settings</string>
|
||||||
|
<string name="nfc_not_default_title">Set Default Payment App</string>
|
||||||
|
<string name="nfc_not_default_message">Set %1$s as the default contactless payment app to use Tap to Pay.</string>
|
||||||
|
<string name="nfc_payment_open_settings">Payment Settings</string>
|
||||||
<string name="card_manage">Manage Card</string>
|
<string name="card_manage">Manage Card</string>
|
||||||
<string name="card_set_as_default">Set as Default Card</string>
|
<string name="card_set_as_default">Set as Default Card</string>
|
||||||
<string name="card_hide_from_dashboard">Hide from Dashboard</string>
|
<string name="card_hide_from_dashboard">Hide from Dashboard</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user