Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fe67aba6e
|
||
|
|
5cba468781
|
@@ -87,24 +87,3 @@ jobs:
|
|||||||
--data-binary "@${ASSET_PATH}"
|
--data-binary "@${ASSET_PATH}"
|
||||||
|
|
||||||
echo "Uploaded asset: $ASSET_NAME"
|
echo "Uploaded asset: $ASSET_NAME"
|
||||||
|
|
||||||
- name: Send APK to Telegram
|
|
||||||
env:
|
|
||||||
TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
|
|
||||||
TG_CHAT_ID: ${{ vars.TG_CHAT_ID }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$TG_BOT_TOKEN" ] || [ -z "$TG_CHAT_ID" ]; then
|
|
||||||
echo "TG_BOT_TOKEN or TG_CHAT_ID not set, skipping Telegram upload."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
APP_NAME="${{ gitea.repository }}"
|
|
||||||
APP_NAME="${APP_NAME##*/}"
|
|
||||||
TAG="${{ gitea.ref_name }}"
|
|
||||||
ASSET_PATH=".build/release/release/${APP_NAME}-${TAG}.apk"
|
|
||||||
CAPTION="${APP_NAME} ${TAG}"
|
|
||||||
|
|
||||||
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendDocument" \
|
|
||||||
-F "chat_id=${TG_CHAT_ID}" \
|
|
||||||
-F "document=@${ASSET_PATH}" \
|
|
||||||
-F "caption=${CAPTION}"
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 9
|
versionCode = 8
|
||||||
versionName = "1.0.10"
|
versionName = "1.0.9"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,7 @@ class BasedBankApp : Application() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// Only apply wallpaper-based dynamic colors in system theme mode.
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
// Light/dark modes use content-based accent colors applied per-activity via ThemeHelper.
|
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this) { _, _ ->
|
|
||||||
getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") == "system"
|
|
||||||
}
|
|
||||||
|
|
||||||
val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system")
|
val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system")
|
||||||
AppCompatDelegate.setDefaultNightMode(when (theme) {
|
AppCompatDelegate.setDefaultNightMode(when (theme) {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.databinding.ActivityLockBinding
|
import sh.sar.basedbank.databinding.ActivityLockBinding
|
||||||
import sh.sar.basedbank.ui.home.HomeActivity
|
import sh.sar.basedbank.ui.home.HomeActivity
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.ThemeHelper
|
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import javax.crypto.SecretKeyFactory
|
import javax.crypto.SecretKeyFactory
|
||||||
import javax.crypto.spec.PBEKeySpec
|
import javax.crypto.spec.PBEKeySpec
|
||||||
@@ -47,7 +46,6 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
ThemeHelper.applyAccent(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityLockBinding.inflate(layoutInflater)
|
binding = ActivityLockBinding.inflate(layoutInflater)
|
||||||
@@ -57,9 +55,6 @@ class LockActivity : AppCompatActivity() {
|
|||||||
isAppearanceLightStatusBars = isLight
|
isAppearanceLightStatusBars = isLight
|
||||||
isAppearanceLightNavigationBars = isLight
|
isAppearanceLightNavigationBars = isLight
|
||||||
}
|
}
|
||||||
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
|
|
||||||
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
|
|
||||||
ta.recycle()
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
||||||
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
|
view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
|
||||||
|
|||||||
@@ -61,13 +61,17 @@ class MibContactsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchContacts(session: MibSession): List<MibBeneficiary> {
|
/** Fetch a single page of contacts. Returns the list and the server-reported total count.
|
||||||
val all = mutableListOf<MibBeneficiary>()
|
* [start] is 1-based; [count] is the desired page size. */
|
||||||
var page = 1
|
fun fetchContactsPage(
|
||||||
val pageSize = 100
|
session: MibSession,
|
||||||
while (true) {
|
start: Int,
|
||||||
val start = (page - 1) * pageSize + 1
|
count: Int
|
||||||
val end = page * pageSize
|
): Pair<List<MibBeneficiary>, Int> {
|
||||||
|
val end = start + count - 1
|
||||||
|
// Server uses 100-row pages internally for its own pagination; we mirror that
|
||||||
|
// by computing a page index that aligns with our start offset.
|
||||||
|
val page = ((start - 1) / 100) + 1
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("page", page.toString())
|
.add("page", page.toString())
|
||||||
.add("search", "")
|
.add("search", "")
|
||||||
@@ -86,13 +90,22 @@ class MibContactsClient {
|
|||||||
.withSessionHeaders(session)
|
.withSessionHeaders(session)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val (contacts, totalCount) = client.newCall(request).execute().use { response ->
|
return client.newCall(request).execute().use { response ->
|
||||||
if (!response.isSuccessful) return all
|
if (!response.isSuccessful) return Pair(emptyList(), 0)
|
||||||
parseContacts(response.body?.string() ?: return all)
|
parseContacts(response.body?.string() ?: return Pair(emptyList(), 0))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: fetch all pages and return the union. Used by callers that don't paginate. */
|
||||||
|
fun fetchContacts(session: MibSession): List<MibBeneficiary> {
|
||||||
|
val all = mutableListOf<MibBeneficiary>()
|
||||||
|
var start = 1
|
||||||
|
val pageSize = 100
|
||||||
|
while (true) {
|
||||||
|
val (contacts, totalCount) = fetchContactsPage(session, start, pageSize)
|
||||||
all.addAll(contacts)
|
all.addAll(contacts)
|
||||||
if (all.size >= totalCount || contacts.isEmpty()) break
|
if (all.size >= totalCount || contacts.isEmpty()) break
|
||||||
page++
|
start += pageSize
|
||||||
}
|
}
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package sh.sar.basedbank.api.mib
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams MIB contacts one page at a time, walking the user's MIB profiles in order.
|
||||||
|
*
|
||||||
|
* For multi-profile users we stay on the current profile until its server-reported
|
||||||
|
* `total_count` is reached, then advance to the next profile. Each profile switch is
|
||||||
|
* protected by the global `mibMutex` to keep the session state coherent.
|
||||||
|
*
|
||||||
|
* Categories are fetched lazily on the first page of each profile and accumulated
|
||||||
|
* across calls — the latest snapshot is exposed via [allCategories].
|
||||||
|
*/
|
||||||
|
class MibContactsPaginator(
|
||||||
|
private val app: BasedBankApp,
|
||||||
|
val loginId: String,
|
||||||
|
private val session: MibSession,
|
||||||
|
private val profiles: List<MibProfile>,
|
||||||
|
private val client: MibContactsClient = MibContactsClient()
|
||||||
|
) {
|
||||||
|
private var profileIndex = 0
|
||||||
|
private var nextStart = 1
|
||||||
|
private var totalForCurrentProfile = -1
|
||||||
|
private var categoriesFetchedForProfile = false
|
||||||
|
|
||||||
|
private val accumulatedCategories = LinkedHashMap<String, BankContactCategory>()
|
||||||
|
val allCategories: List<BankContactCategory>
|
||||||
|
get() = accumulatedCategories.values.toList()
|
||||||
|
|
||||||
|
fun hasMore(): Boolean = profileIndex < profiles.size
|
||||||
|
|
||||||
|
/** Fetch one page of up to [count] contacts. Returns the contacts contributed by
|
||||||
|
* this call (may be empty if a profile errored or returned no rows). Advances
|
||||||
|
* the internal cursor; subsequent calls continue from where this left off. */
|
||||||
|
suspend fun nextPage(count: Int): List<BankContact> = withContext(Dispatchers.IO) {
|
||||||
|
if (!hasMore()) return@withContext emptyList()
|
||||||
|
|
||||||
|
val profile = profiles[profileIndex]
|
||||||
|
val flow = app.mibFlowFor(loginId)
|
||||||
|
|
||||||
|
val contacts = app.mibMutex.withLock {
|
||||||
|
try {
|
||||||
|
flow.switchProfile(session, profile)
|
||||||
|
if (!categoriesFetchedForProfile) {
|
||||||
|
for (cat in client.fetchCategories(session)) {
|
||||||
|
accumulatedCategories.putIfAbsent(
|
||||||
|
cat.id,
|
||||||
|
BankContactCategory(cat.id, cat.categoryName, cat.numBenef)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
categoriesFetchedForProfile = true
|
||||||
|
}
|
||||||
|
val (page, total) = client.fetchContactsPage(session, nextStart, count)
|
||||||
|
if (total > 0) totalForCurrentProfile = total
|
||||||
|
nextStart += page.size.coerceAtLeast(count)
|
||||||
|
page.map { it.copy(profileId = profile.profileId) }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Bail on this profile and advance — keeps the paginator robust.
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val exhausted =
|
||||||
|
(totalForCurrentProfile > 0 && nextStart - 1 >= totalForCurrentProfile) ||
|
||||||
|
contacts.isEmpty()
|
||||||
|
if (exhausted) {
|
||||||
|
profileIndex++
|
||||||
|
nextStart = 1
|
||||||
|
totalForCurrentProfile = -1
|
||||||
|
categoriesFetchedForProfile = false
|
||||||
|
}
|
||||||
|
contacts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -20,7 +21,7 @@ class ContactsAdapter(
|
|||||||
private val onImageNeeded: (hash: String) -> Unit,
|
private val onImageNeeded: (hash: String) -> Unit,
|
||||||
private val onDeleteClick: (ContactDisplay) -> Unit,
|
private val onDeleteClick: (ContactDisplay) -> Unit,
|
||||||
private val onTransferClick: (ContactDisplay) -> Unit
|
private val onTransferClick: (ContactDisplay) -> Unit
|
||||||
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private var allContacts: List<ContactDisplay> = emptyList()
|
private var allContacts: List<ContactDisplay> = emptyList()
|
||||||
private var displayed: List<ContactDisplay> = emptyList()
|
private var displayed: List<ContactDisplay> = emptyList()
|
||||||
@@ -28,6 +29,15 @@ class ContactsAdapter(
|
|||||||
private var activeCategoryId: String? = null
|
private var activeCategoryId: String? = null
|
||||||
private var searchQuery: String = ""
|
private var searchQuery: String = ""
|
||||||
|
|
||||||
|
/** When true, a loading footer row is shown after the last contact. */
|
||||||
|
var showLoadingFooter: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
field = value
|
||||||
|
if (value) notifyItemInserted(displayed.size)
|
||||||
|
else notifyItemRemoved(displayed.size)
|
||||||
|
}
|
||||||
|
|
||||||
fun updateContacts(contacts: List<ContactDisplay>) {
|
fun updateContacts(contacts: List<ContactDisplay>) {
|
||||||
allContacts = contacts
|
allContacts = contacts
|
||||||
applyFilter()
|
applyFilter()
|
||||||
@@ -58,24 +68,32 @@ class ContactsAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun getItemViewType(position: Int): Int =
|
||||||
|
if (position == displayed.size && showLoadingFooter) VIEW_TYPE_FOOTER else VIEW_TYPE_CONTACT
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
if (viewType == VIEW_TYPE_FOOTER) {
|
||||||
|
val footer = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_loading_footer, parent, false)
|
||||||
|
return FooterHolder(footer)
|
||||||
|
}
|
||||||
val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
val holder = ViewHolder(binding)
|
val holder = ViewHolder(binding)
|
||||||
|
|
||||||
binding.btnTransferContact.setOnClickListener {
|
binding.btnTransferContact.setOnClickListener {
|
||||||
val pos = holder.bindingAdapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos != RecyclerView.NO_POSITION) onTransferClick(displayed[pos])
|
if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onTransferClick(displayed[pos])
|
||||||
}
|
}
|
||||||
binding.btnEditContact.setOnClickListener {
|
binding.btnEditContact.setOnClickListener {
|
||||||
Toast.makeText(it.context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
Toast.makeText(it.context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
binding.btnDeleteContact.setOnClickListener {
|
binding.btnDeleteContact.setOnClickListener {
|
||||||
val pos = holder.bindingAdapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos != RecyclerView.NO_POSITION) onDeleteClick(displayed[pos])
|
if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onDeleteClick(displayed[pos])
|
||||||
}
|
}
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
val pos = holder.bindingAdapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
if (pos == RecyclerView.NO_POSITION || pos >= displayed.size) return@setOnLongClickListener false
|
||||||
val account = displayed[pos].accountNumber
|
val account = displayed[pos].accountNumber
|
||||||
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
|
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
|
||||||
@@ -86,15 +104,23 @@ class ContactsAdapter(
|
|||||||
return holder
|
return holder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
if (holder is FooterHolder) return
|
||||||
val contact = displayed[position]
|
val contact = displayed[position]
|
||||||
val cachedImage = contact.imageHash?.let { hash ->
|
val cachedImage = contact.imageHash?.let { hash ->
|
||||||
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
||||||
}
|
}
|
||||||
holder.bind(contact, cachedImage)
|
(holder as ViewHolder).bind(contact, cachedImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = displayed.size
|
override fun getItemCount() = displayed.size + if (showLoadingFooter) 1 else 0
|
||||||
|
|
||||||
|
private class FooterHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val VIEW_TYPE_CONTACT = 0
|
||||||
|
const val VIEW_TYPE_FOOTER = 1
|
||||||
|
}
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemContactBinding) :
|
inner class ViewHolder(val binding: ItemContactBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ class ContactsFragment : Fragment() {
|
|||||||
fun updateImage(hash: String, bitmap: Bitmap) =
|
fun updateImage(hash: String, bitmap: Bitmap) =
|
||||||
contactAdapters.forEach { it.updateImage(hash, bitmap) }
|
contactAdapters.forEach { it.updateImage(hash, bitmap) }
|
||||||
|
|
||||||
|
fun setLoadingFooter(visible: Boolean) =
|
||||||
|
contactAdapters.forEach { it.showLoadingFooter = visible }
|
||||||
|
|
||||||
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
|
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
|
||||||
|
|
||||||
override fun getItemCount() = pages.size
|
override fun getItemCount() = pages.size
|
||||||
@@ -93,6 +96,7 @@ class ContactsFragment : Fragment() {
|
|||||||
val p80 = (65 * density).toInt()
|
val p80 = (65 * density).toInt()
|
||||||
setPadding(0, p4, 0, p80)
|
setPadding(0, p4, 0, p80)
|
||||||
adapter = contactAdapters[viewType]
|
adapter = contactAdapters[viewType]
|
||||||
|
addOnScrollListener(scrollListener)
|
||||||
}
|
}
|
||||||
return PageHolder(rv)
|
return PageHolder(rv)
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,24 @@ class ContactsFragment : Fragment() {
|
|||||||
override fun onBindViewHolder(holder: PageHolder, position: Int) {}
|
override fun onBindViewHolder(holder: PageHolder, position: Int) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Triggers paginated MIB contact loading when the user scrolls within
|
||||||
|
* LOAD_MORE_THRESHOLD rows of the end of any tab. */
|
||||||
|
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (dy <= 0) return
|
||||||
|
val lm = rv.layoutManager as? LinearLayoutManager ?: return
|
||||||
|
val total = lm.itemCount
|
||||||
|
if (total == 0) return
|
||||||
|
if (lm.findLastVisibleItemPosition() >= total - LOAD_MORE_THRESHOLD) {
|
||||||
|
(activity as? HomeActivity)?.loadMoreMibContacts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val LOAD_MORE_THRESHOLD = 5
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -149,6 +171,15 @@ class ContactsFragment : Fragment() {
|
|||||||
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
||||||
binding.loadingView.visibility = View.GONE
|
binding.loadingView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show a loading footer on each contact list while more MIB pages are arriving.
|
||||||
|
val updateFooter: () -> Unit = {
|
||||||
|
val loading = viewModel.mibContactsLoading.value == true
|
||||||
|
val hasMore = viewModel.mibContactsHasMore.value == true
|
||||||
|
pagerAdapter.setLoadingFooter(loading && hasMore)
|
||||||
|
}
|
||||||
|
viewModel.mibContactsLoading.observe(viewLifecycleOwner) { updateFooter() }
|
||||||
|
viewModel.mibContactsHasMore.observe(viewLifecycleOwner) { updateFooter() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachMediator(pages: List<TabPage>) {
|
private fun attachMediator(pages: List<TabPage>) {
|
||||||
@@ -165,6 +196,10 @@ class ContactsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val savedPosition = binding.viewPager.currentItem
|
val savedPosition = binding.viewPager.currentItem
|
||||||
pagerAdapter = ContactsPagerAdapter(pages)
|
pagerAdapter = ContactsPagerAdapter(pages)
|
||||||
|
pagerAdapter.setLoadingFooter(
|
||||||
|
(viewModel.mibContactsLoading.value == true) &&
|
||||||
|
(viewModel.mibContactsHasMore.value == true)
|
||||||
|
)
|
||||||
binding.viewPager.adapter = pagerAdapter
|
binding.viewPager.adapter = pagerAdapter
|
||||||
attachMediator(pages)
|
attachMediator(pages)
|
||||||
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import sh.sar.basedbank.api.models.BankAccount
|
|||||||
import sh.sar.basedbank.api.mib.MibCard
|
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 kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||||
@@ -102,13 +101,8 @@ class DashboardFragment : Fragment() {
|
|||||||
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) }
|
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
.map { CardItem.Bml(it) }
|
.map { CardItem.Bml(it) }
|
||||||
val all = mibItems + bmlItems
|
val all = mibItems + bmlItems
|
||||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
cardAdapter.update(all)
|
||||||
val ordered = if (defaultNum != null) {
|
binding.sectionCards.visibility = if (all.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
|
||||||
if (def != null) listOf(def) + all.filter { it !== def } else all
|
|
||||||
} else all
|
|
||||||
cardAdapter.update(ordered)
|
|
||||||
binding.sectionCards.visibility = if (ordered.isNotEmpty()) View.VISIBLE else View.GONE
|
|
||||||
}
|
}
|
||||||
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
@@ -284,23 +278,26 @@ class DashboardFragment : Fragment() {
|
|||||||
.groupBy({ it.first }, { it.second })
|
.groupBy({ it.first }, { it.second })
|
||||||
.mapValues { (_, vs) -> vs.sum() }
|
.mapValues { (_, vs) -> vs.sum() }
|
||||||
|
|
||||||
val blockedMvr = blockedByCurrency["MVR"] ?: 0.0
|
|
||||||
val blockedUsd = blockedByCurrency["USD"] ?: 0.0
|
|
||||||
val blockedTotal = blockedByCurrency.values.sum()
|
val blockedTotal = blockedByCurrency.values.sum()
|
||||||
|
if (blockedTotal > 0.0) {
|
||||||
|
// Primary line: prefer MVR if present, otherwise the first currency.
|
||||||
|
val primaryCcy = if ("MVR" in blockedByCurrency) "MVR" else blockedByCurrency.keys.first()
|
||||||
|
val primaryAmt = blockedByCurrency.getValue(primaryCcy)
|
||||||
|
binding.tvBlockedTotal.text = if (hide) "$primaryCcy ••••••" else "$primaryCcy %,.2f".format(primaryAmt)
|
||||||
|
|
||||||
if (blockedMvr > 0.0) {
|
val secondary = blockedByCurrency.filterKeys { it != primaryCcy }
|
||||||
binding.tvBlockedMvr.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(blockedMvr)
|
if (secondary.isNotEmpty()) {
|
||||||
binding.cardBlockedMvr.visibility = View.VISIBLE
|
binding.tvBlockedSecondary.text = secondary.entries.joinToString(" · ") { (ccy, amt) ->
|
||||||
} else {
|
if (hide) "$ccy ••••••" else "$ccy %,.2f".format(amt)
|
||||||
binding.cardBlockedMvr.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
if (blockedUsd > 0.0) {
|
binding.tvBlockedSecondary.visibility = View.VISIBLE
|
||||||
binding.tvBlockedUsd.text = if (hide) "USD ••••••" else "USD %,.2f".format(blockedUsd)
|
|
||||||
binding.cardBlockedUsd.visibility = View.VISIBLE
|
|
||||||
} else {
|
} else {
|
||||||
binding.cardBlockedUsd.visibility = View.GONE
|
binding.tvBlockedSecondary.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.cardBlocked.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.cardBlocked.visibility = View.GONE
|
||||||
}
|
}
|
||||||
binding.rowBlocked.visibility = if (blockedTotal > 0.0) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
// Overdue: MIB finance deals + BML loan details (assumed MVR — matches existing Pending Finances).
|
// Overdue: MIB finance deals + BML loan details (assumed MVR — matches existing Pending Finances).
|
||||||
val mibOverdue = (viewModel.financing.value ?: emptyList()).sumOf { it.overdueAmount }
|
val mibOverdue = (viewModel.financing.value ?: emptyList()).sumOf { it.overdueAmount }
|
||||||
@@ -313,7 +310,8 @@ class DashboardFragment : Fragment() {
|
|||||||
binding.cardOverdue.visibility = View.GONE
|
binding.cardOverdue.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.rowAttention.visibility = if (overdueTotal > 0.0) View.VISIBLE else View.GONE
|
binding.rowAttention.visibility =
|
||||||
|
if (blockedTotal > 0.0 || overdueTotal > 0.0) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingFinances() {
|
private fun updatePendingFinances() {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
|
|||||||
import sh.sar.basedbank.api.models.BankContact
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.models.BankContactCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
|
import sh.sar.basedbank.api.mib.MibContactsPaginator
|
||||||
import sh.sar.basedbank.api.mib.MibCardsClient
|
import sh.sar.basedbank.api.mib.MibCardsClient
|
||||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
@@ -68,12 +69,15 @@ import sh.sar.basedbank.util.CredentialStore
|
|||||||
import sh.sar.basedbank.util.CardsCache
|
import sh.sar.basedbank.util.CardsCache
|
||||||
import sh.sar.basedbank.util.FinancingCache
|
import sh.sar.basedbank.util.FinancingCache
|
||||||
import sh.sar.basedbank.util.ForeignLimitsCache
|
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||||
import sh.sar.basedbank.util.ThemeHelper
|
|
||||||
|
|
||||||
class HomeActivity : AppCompatActivity() {
|
class HomeActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private val viewModel: HomeViewModel by viewModels()
|
private val viewModel: HomeViewModel by viewModels()
|
||||||
|
|
||||||
|
/** One paginator per MIB login. Reset on every refresh. */
|
||||||
|
private var mibContactPaginators: List<MibContactsPaginator> = emptyList()
|
||||||
|
private var mibLoadMoreInFlight = false
|
||||||
private lateinit var toggle: ActionBarDrawerToggle
|
private lateinit var toggle: ActionBarDrawerToggle
|
||||||
private var suppressBottomNavCallback = false
|
private var suppressBottomNavCallback = false
|
||||||
|
|
||||||
@@ -107,7 +111,6 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
ThemeHelper.applyAccent(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
@@ -120,9 +123,6 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
isAppearanceLightStatusBars = isLight
|
isAppearanceLightStatusBars = isLight
|
||||||
isAppearanceLightNavigationBars = isLight
|
isAppearanceLightNavigationBars = isLight
|
||||||
}
|
}
|
||||||
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
|
|
||||||
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
|
|
||||||
ta.recycle()
|
|
||||||
// Auth guard: HomeActivity must only be reachable after LockActivity or fresh login.
|
// Auth guard: HomeActivity must only be reachable after LockActivity or fresh login.
|
||||||
// Using loadSecurityHash() (EncryptedSharedPreferences) instead of plain prefs so
|
// Using loadSecurityHash() (EncryptedSharedPreferences) instead of plain prefs so
|
||||||
// a rooted device cannot bypass this by editing security_method in plain prefs.
|
// a rooted device cannot bypass this by editing security_method in plain prefs.
|
||||||
@@ -255,9 +255,6 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
binding.drawerLayout.closeDrawers()
|
binding.drawerLayout.closeDrawers()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Let CardsFragment handle back if in manage mode
|
|
||||||
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
|
|
||||||
if (currentFrag is CardsFragment && currentFrag.onBackPressed()) return
|
|
||||||
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
|
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
supportFragmentManager.popBackStack()
|
supportFragmentManager.popBackStack()
|
||||||
@@ -266,6 +263,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
// In bottom nav mode, pressing back navigates up the hierarchy
|
// In bottom nav mode, pressing back navigates up the hierarchy
|
||||||
val isBottomNav = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
val isBottomNav = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
if (isBottomNav && binding.bottomNavigation.selectedItemId != R.id.nav_dashboard) {
|
if (isBottomNav && binding.bottomNavigation.selectedItemId != R.id.nav_dashboard) {
|
||||||
|
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
|
||||||
// Sub-page reached via More (e.g. Settings, Activities) — go back to More
|
// Sub-page reached via More (e.g. Settings, Activities) — go back to More
|
||||||
if (binding.bottomNavigation.selectedItemId == R.id.nav_more && currentFrag !is MoreFragment) {
|
if (binding.bottomNavigation.selectedItemId == R.id.nav_more && currentFrag !is MoreFragment) {
|
||||||
show(MoreFragment())
|
show(MoreFragment())
|
||||||
@@ -519,8 +517,9 @@ fun applyNavLabelVisibility() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.toolbar_menu, menu)
|
menuInflater.inflate(R.menu.toolbar_menu, menu)
|
||||||
|
val eyeEnabled = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_sensitive_info", false)
|
||||||
val eyeItem = menu.findItem(R.id.action_hide_amounts)
|
val eyeItem = menu.findItem(R.id.action_hide_amounts)
|
||||||
eyeItem?.isVisible = true
|
eyeItem?.isVisible = eyeEnabled
|
||||||
val hidden = viewModel.hideAmounts.value ?: false
|
val hidden = viewModel.hideAmounts.value ?: false
|
||||||
eyeItem?.setIcon(if (hidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility)
|
eyeItem?.setIcon(if (hidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility)
|
||||||
return true
|
return true
|
||||||
@@ -929,42 +928,76 @@ fun applyNavLabelVisibility() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Spin up (or replace) the paginator for this MIB login and fetch the first page.
|
||||||
|
* Subsequent pages arrive via [loadMoreMibContacts] in response to user scrolling. */
|
||||||
private fun refreshContacts(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
private fun refreshContacts(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
val paginator = MibContactsPaginator(
|
||||||
val contactsClient = MibContactsClient()
|
app = application as BasedBankApp,
|
||||||
|
loginId = loginId,
|
||||||
|
session = session,
|
||||||
|
profiles = profiles
|
||||||
|
)
|
||||||
|
// Replace any existing paginator for this login.
|
||||||
|
mibContactPaginators = mibContactPaginators.filter { it.loginId != loginId } + paginator
|
||||||
|
viewModel.mibContactsHasMore.value = mibContactPaginators.any { it.hasMore() }
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
viewModel.mibContactsLoading.postValue(true)
|
||||||
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
|
val firstPage = try {
|
||||||
val seenContacts = mutableSetOf<String>()
|
paginator.nextPage(MIB_FIRST_PAGE_SIZE)
|
||||||
val seenCategories = mutableSetOf<String>()
|
} catch (_: Exception) {
|
||||||
val contacts = mutableListOf<BankContact>()
|
null
|
||||||
val categories = mutableListOf<BankContactCategory>()
|
|
||||||
for (profile in profiles) {
|
|
||||||
try {
|
|
||||||
flow.switchProfile(session, profile)
|
|
||||||
for (cat in contactsClient.fetchCategories(session)) {
|
|
||||||
if (seenCategories.add(cat.id)) categories.add(cat)
|
|
||||||
}
|
}
|
||||||
for (contact in contactsClient.fetchContacts(session)) {
|
// Treat a successful refresh as authoritative: replace the cached MIB list
|
||||||
if (seenContacts.add(contact.benefNo))
|
// with the first page, even if it shrinks (so deletions take effect).
|
||||||
contacts.add(contact.copy(profileId = profile.profileId))
|
if (firstPage != null) {
|
||||||
|
ContactsCache.save(this@HomeActivity, firstPage, paginator.allCategories)
|
||||||
|
publishContacts(paginator.allCategories)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { /* profile has no contacts access */ }
|
viewModel.mibContactsLoading.postValue(false)
|
||||||
|
viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
|
||||||
}
|
}
|
||||||
Pair(contacts, categories)
|
|
||||||
}
|
}
|
||||||
if (allContacts.isNotEmpty()) {
|
|
||||||
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
/** Called by ContactsFragment when the user scrolls near the end of the loaded list. */
|
||||||
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
|
fun loadMoreMibContacts() {
|
||||||
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds())
|
if (mibLoadMoreInFlight) return
|
||||||
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
val paginator = mibContactPaginators.firstOrNull { it.hasMore() } ?: return
|
||||||
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
mibLoadMoreInFlight = true
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
lifecycleScope.launch {
|
||||||
viewModel.contactCategories.postValue(allCategories + fahipayCategories)
|
viewModel.mibContactsLoading.postValue(true)
|
||||||
|
val page = try {
|
||||||
|
paginator.nextPage(MIB_NEXT_PAGE_SIZE)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { /* keep cached data */ }
|
if (page.isNotEmpty()) {
|
||||||
|
ContactsCache.appendContacts(this@HomeActivity, page, paginator.allCategories)
|
||||||
|
publishContacts(paginator.allCategories)
|
||||||
}
|
}
|
||||||
|
mibLoadMoreInFlight = false
|
||||||
|
viewModel.mibContactsLoading.postValue(false)
|
||||||
|
viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merge the current MIB cache with BML and Fahipay caches and post to the view model. */
|
||||||
|
private fun publishContacts(mibCategories: List<BankContactCategory>) {
|
||||||
|
val store = sh.sar.basedbank.util.CredentialStore(this)
|
||||||
|
val mibContacts = ContactsCache.loadContacts(this)
|
||||||
|
val bmlContacts = ContactsCache.loadBml(this, store.getBmlLoginIds())
|
||||||
|
val fahipayContacts = ContactsCache.loadFahipay(this)
|
||||||
|
val fahipayCategories = ContactsCache.loadFahipayCategories(this)
|
||||||
|
viewModel.contacts.postValue(
|
||||||
|
mergeContacts(mergeContacts(mibContacts, bmlContacts), fahipayContacts)
|
||||||
|
)
|
||||||
|
viewModel.contactCategories.postValue(mibCategories + fahipayCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MIB_FIRST_PAGE_SIZE = 30
|
||||||
|
private const val MIB_NEXT_PAGE_SIZE = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshBalances(src: BankAccount) {
|
fun refreshBalances(src: BankAccount) {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ class HomeViewModel : ViewModel() {
|
|||||||
val contacts = MutableLiveData<List<BankContact>>(emptyList())
|
val contacts = MutableLiveData<List<BankContact>>(emptyList())
|
||||||
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
|
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
|
||||||
|
|
||||||
|
/** True while MIB contacts are paging in (initial load or scroll-triggered next page). */
|
||||||
|
val mibContactsLoading = MutableLiveData<Boolean>(false)
|
||||||
|
/** True while any MIB paginator still has pages to deliver. */
|
||||||
|
val mibContactsHasMore = MutableLiveData<Boolean>(false)
|
||||||
|
|
||||||
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||||
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -47,16 +42,6 @@ class OtpFragment : Fragment() {
|
|||||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
holder.b.tvOtpLabel.text = entries[position].label
|
holder.b.tvOtpLabel.text = entries[position].label
|
||||||
update(holder.b, entries[position].seed)
|
update(holder.b, entries[position].seed)
|
||||||
holder.b.root.setOnClickListener {
|
|
||||||
val code = holder.b.tvOtpCode.text.toString().replace(" ", "")
|
|
||||||
if (code.isNotEmpty()) {
|
|
||||||
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("OTP", code))
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Toast.makeText(it.context, "OTP copied", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tick() {
|
fun tick() {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import sh.sar.basedbank.BasedBankApp
|
|||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
import sh.sar.basedbank.util.AccountListParser
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
@@ -99,8 +98,6 @@ class PayMvQrFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
setupDropdown()
|
setupDropdown()
|
||||||
binding.etAmount.addTextChangedListener { scheduleGenerate() }
|
binding.etAmount.addTextChangedListener { scheduleGenerate() }
|
||||||
binding.etReference.addTextChangedListener { scheduleGenerate() }
|
|
||||||
binding.switchIncludePhone.setOnCheckedChangeListener { _, _ -> scheduleGenerate() }
|
|
||||||
binding.btnShare.isEnabled = false
|
binding.btnShare.isEnabled = false
|
||||||
binding.btnSave.isEnabled = false
|
binding.btnSave.isEnabled = false
|
||||||
binding.btnShare.setOnClickListener { shareQr() }
|
binding.btnShare.setOnClickListener { shareQr() }
|
||||||
@@ -113,9 +110,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
private fun setupDropdown() {
|
private fun setupDropdown() {
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||||
val eligible = accounts.filter {
|
val eligible = accounts.filter {
|
||||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" &&
|
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN"
|
||||||
it.bank != "MIB" && // TODO: MIB does not support PayMV QR
|
|
||||||
!(it.bank == "BML" && it.currencyName.contains("USD", ignoreCase = true)) // TODO: BML USD not supported by MMA
|
|
||||||
}
|
}
|
||||||
val adapter = QrAccountAdapter(requireContext(), eligible)
|
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||||
binding.actvAccount.setAdapter(adapter)
|
binding.actvAccount.setAdapter(adapter)
|
||||||
@@ -150,28 +145,8 @@ class PayMvQrFragment : Fragment() {
|
|||||||
?.let { "%.2f".format(it) }
|
?.let { "%.2f".format(it) }
|
||||||
|
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val includePhone = binding.switchIncludePhone.isChecked
|
|
||||||
val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(account.loginTag)
|
|
||||||
val store = CredentialStore(ctx)
|
|
||||||
val mobile = if (includePhone) {
|
|
||||||
when (account.bank) {
|
|
||||||
"BML" -> store.loadBmlUserProfile(loginId)?.mobile
|
|
||||||
"FAHIPAY" -> store.loadFahipayUserProfile(loginId)?.mobile
|
|
||||||
else -> null
|
|
||||||
}?.let { m ->
|
|
||||||
when {
|
|
||||||
m.startsWith("+") -> m
|
|
||||||
m.length == 7 -> "+960$m"
|
|
||||||
else -> m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
|
|
||||||
val purpose = binding.etReference.text?.toString()?.trim()
|
|
||||||
?.takeIf { it.isNotBlank() } ?: getString(R.string.paymvqr_reference_default)
|
|
||||||
|
|
||||||
val bmp = withContext(Dispatchers.Default) {
|
val bmp = withContext(Dispatchers.Default) {
|
||||||
val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted, mobile, purpose)
|
val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted)
|
||||||
renderQrCard(ctx, account, payload, amountFormatted)
|
renderQrCard(ctx, account, payload, amountFormatted)
|
||||||
}
|
}
|
||||||
if (_binding == null) return
|
if (_binding == null) return
|
||||||
@@ -189,9 +164,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
accountNumber: String,
|
accountNumber: String,
|
||||||
accountName: String,
|
accountName: String,
|
||||||
acquirer: String,
|
acquirer: String,
|
||||||
amountStr: String?,
|
amountStr: String?
|
||||||
mobile: String?,
|
|
||||||
purpose: String
|
|
||||||
): String {
|
): String {
|
||||||
fun tlv(tag: String, value: String): String {
|
fun tlv(tag: String, value: String): String {
|
||||||
val len = value.length
|
val len = value.length
|
||||||
@@ -201,30 +174,17 @@ class PayMvQrFragment : Fragment() {
|
|||||||
val poi = tlv("01", "11")
|
val poi = tlv("01", "11")
|
||||||
val sub00 = tlv("00", "mv.favara.mpqr")
|
val sub00 = tlv("00", "mv.favara.mpqr")
|
||||||
val sub01 = tlv("01", acquirer)
|
val sub01 = tlv("01", acquirer)
|
||||||
val sub02 = tlv("02", acquirer) // repeated acquirer, as per official PayMV app
|
|
||||||
val sub03 = tlv("03", accountNumber)
|
val sub03 = tlv("03", accountNumber)
|
||||||
val sub05 = if (!mobile.isNullOrBlank()) tlv("05", mobile) else ""
|
|
||||||
val sub10 = tlv("10", "IPAY")
|
val sub10 = tlv("10", "IPAY")
|
||||||
val merchantAcct = tlv("26", sub00 + sub01 + sub02 + sub03 + sub05 + sub10)
|
val merchantAcct = tlv("26", sub00 + sub01 + sub03 + sub10)
|
||||||
val mcc = tlv("52", "0000")
|
|
||||||
val currency = tlv("53", "462")
|
val currency = tlv("53", "462")
|
||||||
val amountTLV = if (!amountStr.isNullOrBlank()) tlv("54", amountStr) else ""
|
val amountTLV = if (!amountStr.isNullOrBlank()) tlv("54", amountStr) else ""
|
||||||
val country = tlv("58", "MV")
|
val country = tlv("58", "MV")
|
||||||
val name = tlv("59", accountName.take(25))
|
val name = tlv("59", accountName.take(25))
|
||||||
val ref = generateReference()
|
val prefix = format + poi + merchantAcct + currency + amountTLV + country + name + "6304"
|
||||||
val addlData = tlv("62", tlv("05", ref) + tlv("08", purpose))
|
|
||||||
val timestamp = java.time.LocalDateTime.now()
|
|
||||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.00000"))
|
|
||||||
val tag80 = tlv("80", tlv("00", "mv.favara.mpqr") + tlv("01", timestamp))
|
|
||||||
val prefix = format + poi + merchantAcct + mcc + currency + amountTLV + country + name + addlData + tag80 + "6304"
|
|
||||||
return prefix + crc16(prefix)
|
return prefix + crc16(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateReference(): String {
|
|
||||||
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
||||||
return (1..9).map { chars.random() }.joinToString("")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun crc16(data: String): String {
|
private fun crc16(data: String): String {
|
||||||
var crc = 0xFFFF
|
var crc = 0xFFFF
|
||||||
for (c in data) {
|
for (c in data) {
|
||||||
@@ -467,7 +427,7 @@ class PayMvQrFragment : Fragment() {
|
|||||||
} else {
|
} else {
|
||||||
b.tvDropdownAccountType.visibility = View.GONE
|
b.tvDropdownAccountType.visibility = View.GONE
|
||||||
}
|
}
|
||||||
b.tvDropdownBalance.visibility = View.GONE
|
b.tvDropdownBalance.text = displayData?.balance ?: ""
|
||||||
b.root.alpha = 1f
|
b.root.alpha = 1f
|
||||||
|
|
||||||
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import android.view.animation.AccelerateInterpolator
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.doOnNextLayout
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -29,7 +26,6 @@ import sh.sar.basedbank.R
|
|||||||
import sh.sar.basedbank.api.mib.MibCard
|
import sh.sar.basedbank.api.mib.MibCard
|
||||||
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
||||||
import sh.sar.basedbank.util.CardsCache
|
import sh.sar.basedbank.util.CardsCache
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@@ -43,19 +39,6 @@ class CardsFragment : Fragment() {
|
|||||||
private var currentCardPosition: Int = 0
|
private var currentCardPosition: Int = 0
|
||||||
private var cardWidth: Int = 0
|
private var cardWidth: Int = 0
|
||||||
private var pendingQrAccountNumber: String? = null
|
private var pendingQrAccountNumber: String? = null
|
||||||
private var isManageMode: Boolean = false
|
|
||||||
|
|
||||||
// Carousel snapshot captured on enter, used to reverse the exit animation
|
|
||||||
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
|
|
||||||
private var carouselCardCenterX = 0f // card center X relative to contentLayout
|
|
||||||
private var carouselTextLayoutTop = 0f // tvSelectedCardType layout top relative to contentLayout
|
|
||||||
|
|
||||||
// Swipe-to-dismiss tracking
|
|
||||||
private var swipeDragStartRawY = 0f
|
|
||||||
private var swipeIsDragging = false
|
|
||||||
|
|
||||||
private lateinit var stackAdapter: CardStackAdapter
|
|
||||||
private val store by lazy { CredentialStore(requireContext()) }
|
|
||||||
|
|
||||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
@@ -81,7 +64,7 @@ class CardsFragment : Fragment() {
|
|||||||
val peekPx = screenW / 8
|
val peekPx = screenW / 8
|
||||||
cardWidth = screenW - 2 * peekPx
|
cardWidth = screenW - 2 * peekPx
|
||||||
|
|
||||||
stackAdapter = CardStackAdapter(cardWidth)
|
val stackAdapter = CardStackAdapter(cardWidth)
|
||||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
binding.rvCards.adapter = stackAdapter
|
binding.rvCards.adapter = stackAdapter
|
||||||
binding.rvCards.setPadding(peekPx, 0, peekPx, 0)
|
binding.rvCards.setPadding(peekPx, 0, peekPx, 0)
|
||||||
@@ -118,8 +101,24 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.mibCards.observe(viewLifecycleOwner) { rebuildCards() }
|
val updateCardList = {
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { rebuildCards() }
|
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
|
||||||
|
val bmlItems = (viewModel.accounts.value ?: emptyList())
|
||||||
|
.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||||
|
.map { CardItem.Bml(it) }
|
||||||
|
cards = mibItems + bmlItems
|
||||||
|
stackAdapter.update(cards)
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
val empty = cards.isEmpty()
|
||||||
|
binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE
|
||||||
|
binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE
|
||||||
|
if (!empty) {
|
||||||
|
buildDots(cards.size, currentCardPosition)
|
||||||
|
updateCardInfo(currentCardPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||||
|
|
||||||
val cached = CardsCache.load(requireContext())
|
val cached = CardsCache.load(requireContext())
|
||||||
if (cached.isNotEmpty()) {
|
if (cached.isNotEmpty()) {
|
||||||
@@ -129,54 +128,6 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
}
|
}
|
||||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||||
|
|
||||||
binding.btnManageCard.setOnClickListener {
|
|
||||||
setManageMode(!isManageMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swipe-down on the manage card to dismiss manage mode
|
|
||||||
binding.manageCardView.root.setOnTouchListener { _, event ->
|
|
||||||
if (!isManageMode) return@setOnTouchListener false
|
|
||||||
val mgr = binding.manageCardView.root
|
|
||||||
when (event.action) {
|
|
||||||
android.view.MotionEvent.ACTION_DOWN -> {
|
|
||||||
mgr.animate().cancel()
|
|
||||||
binding.tvSelectedCardType.animate().cancel()
|
|
||||||
swipeDragStartRawY = event.rawY
|
|
||||||
swipeIsDragging = false
|
|
||||||
true
|
|
||||||
}
|
|
||||||
android.view.MotionEvent.ACTION_MOVE -> {
|
|
||||||
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
|
||||||
if (dy > 12f || swipeIsDragging) {
|
|
||||||
swipeIsDragging = true
|
|
||||||
mgr.translationY = dy
|
|
||||||
binding.tvSelectedCardType.translationY = dy * 0.6f
|
|
||||||
val scale = 1f - (dy / (binding.contentLayout.height * 2.5f)).coerceIn(0f, 0.12f)
|
|
||||||
mgr.scaleX = scale
|
|
||||||
mgr.scaleY = scale
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
||||||
if (swipeIsDragging) {
|
|
||||||
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
|
||||||
swipeIsDragging = false
|
|
||||||
if (dy > 130f) {
|
|
||||||
setManageMode(false)
|
|
||||||
} else {
|
|
||||||
// Snap back
|
|
||||||
mgr.animate().translationY(0f).scaleX(1f).scaleY(1f)
|
|
||||||
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
|
||||||
binding.tvSelectedCardType.animate().translationY(0f)
|
|
||||||
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnScanToPay.setOnClickListener {
|
binding.btnScanToPay.setOnClickListener {
|
||||||
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
||||||
if (item is CardItem.Mib) {
|
if (item is CardItem.Mib) {
|
||||||
@@ -203,216 +154,6 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
binding.btnBlock.setOnClickListener(wip)
|
binding.btnBlock.setOnClickListener(wip)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setManageMode(enabled: Boolean) {
|
|
||||||
isManageMode = enabled
|
|
||||||
requireActivity().title = getString(if (enabled) R.string.card_manage else R.string.nav_pay_with_card)
|
|
||||||
if (enabled) enterManageMode() else exitManageMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enterManageMode() {
|
|
||||||
val item = cards.getOrNull(currentCardPosition) ?: return
|
|
||||||
|
|
||||||
// Bind card data
|
|
||||||
val cv = binding.manageCardView
|
|
||||||
when (item) {
|
|
||||||
is CardItem.Mib -> {
|
|
||||||
cv.tvCardOwner.text = item.card.cardHolderName
|
|
||||||
cv.tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
|
||||||
val assetPath = cardImageAsset(item.card)
|
|
||||||
if (assetPath != null) loadCardImage(cv.ivCardImage, assetPath)
|
|
||||||
else cv.ivCardImage.setImageDrawable(null)
|
|
||||||
bindCardStatus(cv.tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
|
||||||
cv.root.alpha = 1f
|
|
||||||
}
|
|
||||||
is CardItem.Bml -> {
|
|
||||||
cv.tvCardOwner.text = item.account.accountBriefName
|
|
||||||
cv.tvCardNumber.text = formatMasked(item.account.accountNumber)
|
|
||||||
loadCardImage(cv.ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
|
||||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
|
||||||
bindCardStatus(cv.tvCardStatus, item.account.statusDesc.takeUnless { isActive })
|
|
||||||
cv.root.alpha = if (isActive) 1f else 0.45f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture positions BEFORE layout changes (for enter animation + exit animation later)
|
|
||||||
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
|
||||||
val lm = binding.rvCards.layoutManager as? LinearLayoutManager
|
|
||||||
val srcView = lm?.findViewByPosition(currentCardPosition)
|
|
||||||
val srcLoc = IntArray(2).also { srcView?.getLocationOnScreen(it) ?: run { it[0] = contentLoc[0]; it[1] = contentLoc[1] } }
|
|
||||||
val srcScreenTop = (srcLoc[1] - contentLoc[1]).toFloat()
|
|
||||||
val srcCenterX = (srcLoc[0] - contentLoc[0]).toFloat() + cardWidth / 2f
|
|
||||||
|
|
||||||
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
|
||||||
val textSrcScreenTop = (textLoc[1] - contentLoc[1]).toFloat()
|
|
||||||
|
|
||||||
// Apply layout changes
|
|
||||||
binding.btnManageCard.visibility = View.GONE
|
|
||||||
binding.topSpacer.visibility = View.GONE
|
|
||||||
binding.rvCards.visibility = View.GONE
|
|
||||||
binding.pageIndicator.visibility = View.GONE
|
|
||||||
binding.llPayButtons.visibility = View.GONE
|
|
||||||
binding.llManageButtons.visibility = View.VISIBLE
|
|
||||||
binding.llDefaultCardRow.visibility = View.VISIBLE
|
|
||||||
binding.manageCardView.root.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// Set switch state (clear listener first to avoid triggering on programmatic set)
|
|
||||||
val isBml = item is CardItem.Bml
|
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
|
||||||
binding.switchDefaultCard.isChecked = isBml && store.getDefaultCardAccountNumber() == (item as? CardItem.Bml)?.account?.accountNumber
|
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
if (item is CardItem.Mib) {
|
|
||||||
// MIB doesn't support NFC/QR pay — same toast as scan/tap to pay
|
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
|
||||||
binding.switchDefaultCard.isChecked = false
|
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener { _, c ->
|
|
||||||
handleDefaultCardToggle(c)
|
|
||||||
}
|
|
||||||
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
handleDefaultCardToggle(isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After layout pass, compute offsets, save carousel snapshot, and animate
|
|
||||||
binding.contentLayout.doOnNextLayout {
|
|
||||||
val mgr = binding.manageCardView.root
|
|
||||||
val dstLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
|
||||||
val dstTop = (dstLoc[1] - contentLoc[1]).toFloat()
|
|
||||||
val dstCenterX = (dstLoc[0] - contentLoc[0]).toFloat() + mgr.width / 2f
|
|
||||||
|
|
||||||
val scaleStart = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
|
||||||
val transXStart = srcCenterX - dstCenterX
|
|
||||||
val transYStart = srcScreenTop - dstTop
|
|
||||||
|
|
||||||
// Save the carousel card's position (relative to contentLayout) for the exit animation
|
|
||||||
carouselCardLayoutTop = srcScreenTop
|
|
||||||
carouselCardCenterX = srcCenterX
|
|
||||||
carouselTextLayoutTop = textSrcScreenTop
|
|
||||||
|
|
||||||
val textDstLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
|
||||||
val textDstTop = (textDstLoc[1] - contentLoc[1]).toFloat()
|
|
||||||
|
|
||||||
mgr.pivotX = mgr.width / 2f
|
|
||||||
mgr.pivotY = 0f
|
|
||||||
mgr.scaleX = scaleStart
|
|
||||||
mgr.scaleY = scaleStart
|
|
||||||
mgr.translationX = transXStart
|
|
||||||
mgr.translationY = transYStart
|
|
||||||
|
|
||||||
mgr.animate()
|
|
||||||
.scaleX(1f).scaleY(1f)
|
|
||||||
.translationX(0f).translationY(0f)
|
|
||||||
.setDuration(380)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.start()
|
|
||||||
|
|
||||||
binding.tvSelectedCardType.translationY = textSrcScreenTop - textDstTop
|
|
||||||
binding.tvSelectedCardType.animate()
|
|
||||||
.translationY(0f)
|
|
||||||
.setDuration(380)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDefaultCardToggle(isChecked: Boolean) {
|
|
||||||
val item = cards.getOrNull(currentCardPosition) as? CardItem.Bml ?: return
|
|
||||||
store.setDefaultCardAccountNumber(if (isChecked) item.account.accountNumber else null)
|
|
||||||
rebuildCards()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exitManageMode() {
|
|
||||||
binding.manageCardView.root.animate().cancel()
|
|
||||||
binding.tvSelectedCardType.animate().cancel()
|
|
||||||
|
|
||||||
val mgr = binding.manageCardView.root
|
|
||||||
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
|
||||||
|
|
||||||
// Compute layout top of manage card (strip current translationY which may be from a swipe drag)
|
|
||||||
val mgrLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
|
||||||
val mgrLayoutTop = (mgrLoc[1] - contentLoc[1]).toFloat() - mgr.translationY
|
|
||||||
|
|
||||||
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
|
||||||
val textLayoutTop = (textLoc[1] - contentLoc[1]).toFloat() - binding.tvSelectedCardType.translationY
|
|
||||||
|
|
||||||
// Target: animate card back to carousel position
|
|
||||||
val scaleEnd = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
|
||||||
val mgrLayoutCenterX = (mgrLoc[0] - contentLoc[0]).toFloat() - mgr.translationX + mgr.width / 2f
|
|
||||||
val targetTransX = carouselCardCenterX - mgrLayoutCenterX
|
|
||||||
val targetTransY = carouselCardLayoutTop - mgrLayoutTop
|
|
||||||
|
|
||||||
val targetTextTransY = carouselTextLayoutTop - textLayoutTop
|
|
||||||
|
|
||||||
mgr.pivotX = mgr.width / 2f
|
|
||||||
mgr.pivotY = 0f
|
|
||||||
|
|
||||||
mgr.animate()
|
|
||||||
.scaleX(scaleEnd).scaleY(scaleEnd)
|
|
||||||
.translationX(targetTransX)
|
|
||||||
.translationY(targetTransY)
|
|
||||||
.setDuration(320)
|
|
||||||
.setInterpolator(AccelerateInterpolator())
|
|
||||||
.withEndAction {
|
|
||||||
mgr.scaleX = 1f; mgr.scaleY = 1f
|
|
||||||
mgr.translationX = 0f; mgr.translationY = 0f
|
|
||||||
mgr.visibility = View.GONE
|
|
||||||
binding.tvSelectedCardType.translationY = 0f
|
|
||||||
|
|
||||||
binding.btnManageCard.visibility = View.VISIBLE
|
|
||||||
binding.topSpacer.visibility = View.VISIBLE
|
|
||||||
binding.rvCards.visibility = View.VISIBLE
|
|
||||||
binding.llPayButtons.visibility = View.VISIBLE
|
|
||||||
binding.llManageButtons.visibility = View.GONE
|
|
||||||
binding.llDefaultCardRow.visibility = View.GONE
|
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
|
||||||
buildDots(cards.size, currentCardPosition)
|
|
||||||
}
|
|
||||||
.start()
|
|
||||||
|
|
||||||
binding.tvSelectedCardType.animate()
|
|
||||||
.translationY(targetTextTransY)
|
|
||||||
.setDuration(320)
|
|
||||||
.setInterpolator(AccelerateInterpolator())
|
|
||||||
.withEndAction { binding.tvSelectedCardType.translationY = 0f }
|
|
||||||
.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rebuildCards() {
|
|
||||||
// Remember which card is currently selected by identity so we can restore position after reorder
|
|
||||||
val currentCard = cards.getOrNull(currentCardPosition)
|
|
||||||
|
|
||||||
val defaultNum = store.getDefaultCardAccountNumber()
|
|
||||||
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
|
|
||||||
val bmlItems = (viewModel.accounts.value ?: emptyList())
|
|
||||||
.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
|
||||||
.map { CardItem.Bml(it) }
|
|
||||||
val all: List<CardItem> = mibItems + bmlItems
|
|
||||||
// Move default BML card to front
|
|
||||||
cards = if (defaultNum != null) {
|
|
||||||
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
|
||||||
if (def != null) listOf(def) + all.filter { it !== def } else all
|
|
||||||
} else all
|
|
||||||
|
|
||||||
// Restore position to follow the same card after reorder
|
|
||||||
if (currentCard != null) {
|
|
||||||
val newPos = cards.indexOf(currentCard)
|
|
||||||
if (newPos >= 0 && newPos != currentCardPosition) {
|
|
||||||
currentCardPosition = newPos
|
|
||||||
binding.rvCards.scrollToPosition(newPos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stackAdapter.update(cards)
|
|
||||||
binding.loadingView.visibility = View.GONE
|
|
||||||
val empty = cards.isEmpty()
|
|
||||||
binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE
|
|
||||||
binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE
|
|
||||||
if (!empty) {
|
|
||||||
buildDots(cards.size, currentCardPosition)
|
|
||||||
updateCardInfo(currentCardPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyCardScales() {
|
private fun applyCardScales() {
|
||||||
val rv = binding.rvCards
|
val rv = binding.rvCards
|
||||||
val rvCenter = rv.paddingStart + (rv.width - rv.paddingStart - rv.paddingEnd) / 2f
|
val rvCenter = rv.paddingStart + (rv.width - rv.paddingStart - rv.paddingEnd) / 2f
|
||||||
@@ -432,7 +173,6 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDots(count: Int, selected: Int) {
|
private fun buildDots(count: Int, selected: Int) {
|
||||||
if (isManageMode) return
|
|
||||||
binding.pageIndicator.removeAllViews()
|
binding.pageIndicator.removeAllViews()
|
||||||
if (count <= 1) {
|
if (count <= 1) {
|
||||||
binding.pageIndicator.visibility = View.GONE
|
binding.pageIndicator.visibility = View.GONE
|
||||||
@@ -467,14 +207,6 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackPressed(): Boolean {
|
|
||||||
if (isManageMode) {
|
|
||||||
setManageMode(false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = getString(R.string.nav_pay_with_card)
|
requireActivity().title = getString(R.string.nav_pay_with_card)
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package sh.sar.basedbank.ui.home
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -12,7 +10,6 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
|
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
@@ -21,11 +18,8 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding
|
import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding
|
||||||
import sh.sar.basedbank.util.ThemeHelper
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
class SettingsAppearanceFragment : Fragment() {
|
class SettingsAppearanceFragment : Fragment() {
|
||||||
@@ -109,45 +103,8 @@ class SettingsAppearanceFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
prefs.edit().putString("theme", key).apply()
|
prefs.edit().putString("theme", key).apply()
|
||||||
AppCompatDelegate.setDefaultNightMode(mode)
|
AppCompatDelegate.setDefaultNightMode(mode)
|
||||||
updateAccentState(key == "system")
|
|
||||||
updatePitchBlackState(key == "dark")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pitch black
|
|
||||||
binding.switchPitchBlack.isChecked = prefs.getBoolean("pitch_black", false)
|
|
||||||
binding.switchPitchBlack.setOnCheckedChangeListener { _, checked ->
|
|
||||||
prefs.edit().putBoolean("pitch_black", checked).apply()
|
|
||||||
requireActivity().recreate()
|
|
||||||
}
|
|
||||||
val isDark = prefs.getString("theme", "system") == "dark"
|
|
||||||
updatePitchBlackState(isDark)
|
|
||||||
|
|
||||||
// Accent color
|
|
||||||
val savedPreset = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE)
|
|
||||||
binding.accentToggle.check(when (savedPreset) {
|
|
||||||
ThemeHelper.PRESET_RED -> R.id.btnAccentOrange
|
|
||||||
ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen
|
|
||||||
ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom
|
|
||||||
else -> R.id.btnAccentBlue
|
|
||||||
})
|
|
||||||
binding.accentToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
|
||||||
if (!isChecked) return@addOnButtonCheckedListener
|
|
||||||
val preset = when (checkedId) {
|
|
||||||
R.id.btnAccentOrange -> ThemeHelper.PRESET_RED
|
|
||||||
R.id.btnAccentGreen -> ThemeHelper.PRESET_GREEN
|
|
||||||
R.id.btnAccentCustom -> ThemeHelper.PRESET_CUSTOM
|
|
||||||
else -> ThemeHelper.PRESET_BLUE
|
|
||||||
}
|
|
||||||
if (preset == ThemeHelper.PRESET_CUSTOM) {
|
|
||||||
showCustomColorPicker()
|
|
||||||
} else {
|
|
||||||
prefs.edit().putString("accent_preset", preset).apply()
|
|
||||||
requireActivity().recreate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val isSystem = prefs.getString("theme", "system") == "system"
|
|
||||||
updateAccentState(isSystem)
|
|
||||||
|
|
||||||
// Language
|
// Language
|
||||||
val currentLocales = AppCompatDelegate.getApplicationLocales()
|
val currentLocales = AppCompatDelegate.getApplicationLocales()
|
||||||
val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en"
|
val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en"
|
||||||
@@ -199,68 +156,6 @@ class SettingsAppearanceFragment : Fragment() {
|
|||||||
slotAdapter.notifyDataSetChanged()
|
slotAdapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePitchBlackState(isDark: Boolean) {
|
|
||||||
binding.rowPitchBlack.alpha = if (isDark) 1f else 0.38f
|
|
||||||
binding.switchPitchBlack.isEnabled = isDark
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateAccentState(isSystem: Boolean) {
|
|
||||||
binding.sectionAccentColor.alpha = if (isSystem) 0.38f else 1f
|
|
||||||
for (i in 0 until binding.accentToggle.childCount) {
|
|
||||||
binding.accentToggle.getChildAt(i)?.isEnabled = !isSystem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCustomColorPicker() {
|
|
||||||
val ctx = requireContext()
|
|
||||||
val currentHex = prefs.getString("accent_custom_color", "") ?: ""
|
|
||||||
val inputLayout = TextInputLayout(ctx).apply {
|
|
||||||
hint = getString(R.string.accent_custom_hint)
|
|
||||||
val pad = (16 * resources.displayMetrics.density).toInt()
|
|
||||||
setPadding(pad, pad / 2, pad, 0)
|
|
||||||
}
|
|
||||||
val input = TextInputEditText(ctx).apply {
|
|
||||||
setText(currentHex)
|
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
||||||
setSingleLine(true)
|
|
||||||
}
|
|
||||||
inputLayout.addView(input)
|
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(R.string.accent_custom_pick)
|
|
||||||
.setView(inputLayout)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> revertAccentToggle() }
|
|
||||||
.setOnCancelListener { revertAccentToggle() }
|
|
||||||
.show()
|
|
||||||
|
|
||||||
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
|
||||||
val raw = input.text.toString().trim()
|
|
||||||
val hex = if (raw.startsWith("#")) raw else "#$raw"
|
|
||||||
try {
|
|
||||||
Color.parseColor(hex)
|
|
||||||
prefs.edit()
|
|
||||||
.putString("accent_preset", ThemeHelper.PRESET_CUSTOM)
|
|
||||||
.putString("accent_custom_color", hex)
|
|
||||||
.apply()
|
|
||||||
dialog.dismiss()
|
|
||||||
requireActivity().recreate()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
Toast.makeText(ctx, R.string.accent_invalid_color, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun revertAccentToggle() {
|
|
||||||
val saved = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE)
|
|
||||||
binding.accentToggle.check(when (saved) {
|
|
||||||
ThemeHelper.PRESET_RED -> R.id.btnAccentOrange
|
|
||||||
ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen
|
|
||||||
ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom
|
|
||||||
else -> R.id.btnAccentBlue
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showItemPicker(items: MutableList<Int>, slotIndex: Int, adapter: NavItemAdapter) {
|
private fun showItemPicker(items: MutableList<Int>, slotIndex: Int, adapter: NavItemAdapter) {
|
||||||
val isBottom = prefs.getBoolean("bottom_nav", false)
|
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||||
if (items === slots && !isBottom) return
|
if (items === slots && !isBottom) return
|
||||||
@@ -273,7 +168,6 @@ class SettingsAppearanceFragment : Fragment() {
|
|||||||
LayoutInflater.from(ctx).inflate(R.layout.item_more_nav, listLayout, false).also { row ->
|
LayoutInflater.from(ctx).inflate(R.layout.item_more_nav, listLayout, false).also { row ->
|
||||||
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
||||||
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
||||||
row.findViewById<TextView>(R.id.tvDescription).visibility = View.GONE
|
|
||||||
listLayout.addView(row)
|
listLayout.addView(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.provider.Settings as AndroidSettings
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -18,7 +15,6 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
@@ -81,29 +77,6 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
handlePickedImage(bitmap)
|
handlePickedImage(bitmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cameraPermissionLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { granted ->
|
|
||||||
if (granted) cameraLauncher.launch(cameraPhotoUri ?: return@registerForActivityResult)
|
|
||||||
else showCameraPermissionRationale()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCameraPermissionRationale() {
|
|
||||||
val ctx = requireContext()
|
|
||||||
MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(R.string.qr_camera_permission_title)
|
|
||||||
.setMessage(R.string.camera_permission_profile_message)
|
|
||||||
.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
|
||||||
.setPositiveButton(R.string.go_to_settings) { _, _ ->
|
|
||||||
startActivity(
|
|
||||||
Intent(AndroidSettings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", ctx.packageName, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadAndScaleBitmap(uri: Uri): Bitmap? {
|
private fun loadAndScaleBitmap(uri: Uri): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
@@ -186,11 +159,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
val photoFile = File(ctx.cacheDir, "profile_photo_tmp.jpg")
|
val photoFile = File(ctx.cacheDir, "profile_photo_tmp.jpg")
|
||||||
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", photoFile)
|
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", photoFile)
|
||||||
cameraPhotoUri = uri
|
cameraPhotoUri = uri
|
||||||
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
cameraLauncher.launch(uri)
|
cameraLauncher.launch(uri)
|
||||||
} else {
|
|
||||||
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (target is PendingImageTarget.Mib || currentBitmap != null || hasSavedImage(ctx, target)) {
|
if (target is PendingImageTarget.Mib || currentBitmap != null || hasSavedImage(ctx, target)) {
|
||||||
items += Triple(R.drawable.ic_delete, getString(R.string.profile_image_remove)) {
|
items += Triple(R.drawable.ic_delete, getString(R.string.profile_image_remove)) {
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ class SettingsSecurityFragment : Fragment() {
|
|||||||
(activity as? HomeActivity)?.resetAutolockTimer()
|
(activity as? HomeActivity)?.resetAutolockTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide sensitive information (enables/disables the eye icon in toolbar)
|
||||||
|
val viewModel = (requireActivity() as HomeActivity).let {
|
||||||
|
androidx.lifecycle.ViewModelProvider(it)[HomeViewModel::class.java]
|
||||||
|
}
|
||||||
|
binding.switchHideAmounts.isChecked = prefs.getBoolean("hide_sensitive_info", false)
|
||||||
|
binding.switchHideAmounts.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.edit().putBoolean("hide_sensitive_info", isChecked).apply()
|
||||||
|
if (!isChecked) viewModel.hideAmounts.value = false
|
||||||
|
requireActivity().invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// Block screenshots
|
// Block screenshots
|
||||||
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
|
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
|
||||||
binding.switchBlockScreenshots.isChecked = blockScreenshots
|
binding.switchBlockScreenshots.isChecked = blockScreenshots
|
||||||
|
|||||||
@@ -127,10 +127,7 @@ class TransferFragment : Fragment() {
|
|||||||
// BML card/gateway QR — hand off to dedicated payment screen
|
// BML card/gateway QR — hand off to dedicated payment screen
|
||||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
||||||
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
||||||
val fromCard = selectedAccount?.takeIf {
|
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw))
|
||||||
it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT"
|
|
||||||
}
|
|
||||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw, fromCard?.accountNumber))
|
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,25 +290,6 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
bmlQrInfo = info
|
bmlQrInfo = info
|
||||||
|
|
||||||
// Auto-select the user's default BML card if no card was pre-selected
|
|
||||||
if (selectedAccount == null) {
|
|
||||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
|
||||||
if (defaultNum != null) {
|
|
||||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
|
||||||
val defaultCard = allAccounts.firstOrNull {
|
|
||||||
it.accountNumber == defaultNum &&
|
|
||||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") &&
|
|
||||||
it.statusDesc.equals("Active", ignoreCase = true)
|
|
||||||
}
|
|
||||||
if (defaultCard != null) {
|
|
||||||
selectedAccount = defaultCard
|
|
||||||
updateAmountPrefix(defaultCard)
|
|
||||||
showFromCard(defaultCard)
|
|
||||||
updateTransferButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show merchant in the "To" card — clear button hidden (can't change recipient for QR)
|
// Show merchant in the "To" card — clear button hidden (can't change recipient for QR)
|
||||||
binding.tvToAccountName.text = info.merchantName
|
binding.tvToAccountName.text = info.merchantName
|
||||||
binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" }
|
binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" }
|
||||||
@@ -319,6 +297,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tvToBalance.visibility = View.GONE
|
binding.tvToBalance.visibility = View.GONE
|
||||||
binding.ivToPhoto.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
binding.ivToPhoto.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(info.merchantName, "#0066A1"))
|
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(info.merchantName, "#0066A1"))
|
||||||
|
binding.btnClearToInfo.visibility = View.GONE
|
||||||
binding.cardToInfo.visibility = View.VISIBLE
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
|
|
||||||
// Pre-fill amount if dynamic QR
|
// Pre-fill amount if dynamic QR
|
||||||
@@ -374,14 +353,6 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||||
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
||||||
if (bmlQrInfo != null) {
|
|
||||||
val isCard = picked.profileType == "BML_PREPAID" || picked.profileType == "BML_CREDIT" || picked.profileType == "BML_DEBIT"
|
|
||||||
if (!isCard) {
|
|
||||||
Toast.makeText(requireContext(), "Unsupported for BML QR — select a card", Toast.LENGTH_SHORT).show()
|
|
||||||
binding.actvFrom.setText("", false)
|
|
||||||
return@setOnItemClickListener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedAccount = picked
|
selectedAccount = picked
|
||||||
updateAmountPrefix(picked)
|
updateAmountPrefix(picked)
|
||||||
showFromCard(picked)
|
showFromCard(picked)
|
||||||
@@ -535,14 +506,6 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
||||||
|
|
||||||
binding.btnClearToInfo.setOnClickListener {
|
binding.btnClearToInfo.setOnClickListener {
|
||||||
if (bmlQrInfo != null) {
|
|
||||||
bmlQrInfo = null
|
|
||||||
bmlGatewayQr = false
|
|
||||||
binding.tilAmount.isEnabled = true
|
|
||||||
binding.tilRemarks.isEnabled = true
|
|
||||||
binding.tilRemarks.alpha = 1f
|
|
||||||
binding.etAmount.setText("")
|
|
||||||
}
|
|
||||||
resolvedAccountNumber = ""
|
resolvedAccountNumber = ""
|
||||||
resolvedRecipientName = ""
|
resolvedRecipientName = ""
|
||||||
resolvedToOwnAccount = null
|
resolvedToOwnAccount = null
|
||||||
@@ -1774,7 +1737,6 @@ class TransferFragment : Fragment() {
|
|||||||
.also { it.root.tag = it }
|
.also { it.root.tag = it }
|
||||||
}
|
}
|
||||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||||
val isCard = acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT"
|
|
||||||
val isBmlAccount = acc.bank == "BML"
|
val isBmlAccount = acc.bank == "BML"
|
||||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||||
val hide = viewModel.hideAmounts.value ?: false
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
@@ -1792,11 +1754,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
val balance = displayData?.balance ?: ""
|
val balance = displayData?.balance ?: ""
|
||||||
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
||||||
b.root.alpha = when {
|
b.root.alpha = if (inactive) 0.4f else 1f
|
||||||
inactive -> 0.4f
|
|
||||||
bmlQrInfo != null && !isCard -> 0.35f
|
|
||||||
else -> 1f
|
|
||||||
}
|
|
||||||
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
||||||
when {
|
when {
|
||||||
networkIcon != null -> {
|
networkIcon != null -> {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package sh.sar.basedbank.ui.login
|
package sh.sar.basedbank.ui.login
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -84,17 +81,6 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.btnLogin.isEnabled = false
|
binding.btnLogin.isEnabled = false
|
||||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||||
|
|
||||||
binding.cardOtp.setOnClickListener {
|
|
||||||
val code = binding.tvOtpCode.text.toString().replace(" ", "")
|
|
||||||
if (code.isNotEmpty()) {
|
|
||||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("OTP", code))
|
|
||||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Toast.makeText(requireContext(), "OTP copied", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val loginFieldWatcher = object : TextWatcher {
|
val loginFieldWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) { updateLoginButtonState() }
|
override fun afterTextChanged(s: Editable?) { updateLoginButtonState() }
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import sh.sar.basedbank.BasedBankApp
|
|||||||
import sh.sar.basedbank.LockActivity
|
import sh.sar.basedbank.LockActivity
|
||||||
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
import sh.sar.basedbank.databinding.ActivityLoginBinding
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.ThemeHelper
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityLoginBinding
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
ThemeHelper.applyAccent(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// If security is configured and the user hasn't unlocked this session,
|
// If security is configured and the user hasn't unlocked this session,
|
||||||
@@ -36,8 +34,5 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
isAppearanceLightStatusBars = isLight
|
isAppearanceLightStatusBars = isLight
|
||||||
isAppearanceLightNavigationBars = isLight
|
isAppearanceLightNavigationBars = isLight
|
||||||
}
|
}
|
||||||
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
|
|
||||||
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
|
|
||||||
ta.recycle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import sh.sar.basedbank.R
|
|||||||
import sh.sar.basedbank.databinding.ActivityOnboardingBinding
|
import sh.sar.basedbank.databinding.ActivityOnboardingBinding
|
||||||
import sh.sar.basedbank.ui.login.LoginActivity
|
import sh.sar.basedbank.ui.login.LoginActivity
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.ThemeHelper
|
|
||||||
|
|
||||||
class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
private var countDownTimer: CountDownTimer? = null
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
ThemeHelper.applyAccent(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// If security is already configured, onboarding is complete. Redirect to lock screen
|
// If security is already configured, onboarding is complete. Redirect to lock screen
|
||||||
@@ -47,9 +45,6 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
isAppearanceLightStatusBars = isLight
|
isAppearanceLightStatusBars = isLight
|
||||||
isAppearanceLightNavigationBars = isLight
|
isAppearanceLightNavigationBars = isLight
|
||||||
}
|
}
|
||||||
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
|
|
||||||
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
|
|
||||||
ta.recycle()
|
|
||||||
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
val originalBottomPadding = binding.bottomBar.paddingBottom
|
val originalBottomPadding = binding.bottomBar.paddingBottom
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ object ContactsCache {
|
|||||||
prefs.apply()
|
prefs.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Merge new MIB contacts into the cached list, deduping by benefNo. Categories
|
||||||
|
* are replaced wholesale (the latest snapshot is canonical). Safe to call
|
||||||
|
* repeatedly as pages arrive. */
|
||||||
|
fun appendContacts(
|
||||||
|
context: Context,
|
||||||
|
newContacts: List<BankContact>,
|
||||||
|
categories: List<BankContactCategory>
|
||||||
|
) {
|
||||||
|
if (newContacts.isEmpty() && categories.isEmpty()) return
|
||||||
|
val existing = loadContacts(context)
|
||||||
|
val seen = existing.map { it.benefNo }.toHashSet()
|
||||||
|
val merged = existing + newContacts.filter { seen.add(it.benefNo) }
|
||||||
|
save(context, merged, categories)
|
||||||
|
}
|
||||||
|
|
||||||
fun clear(context: Context) {
|
fun clear(context: Context) {
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,18 +615,6 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Default payment card ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** BML card account number the user has pinned as their default payment card, or null. */
|
|
||||||
fun getDefaultCardAccountNumber(): String? = prefs.getString("default_card_account_number", null)
|
|
||||||
|
|
||||||
fun setDefaultCardAccountNumber(accountNumber: String?) {
|
|
||||||
val editor = prefs.edit()
|
|
||||||
if (accountNumber == null) editor.remove("default_card_account_number")
|
|
||||||
else editor.putString("default_card_account_number", accountNumber)
|
|
||||||
editor.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
||||||
|
|
||||||
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
|
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package sh.sar.basedbank.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.google.android.material.color.DynamicColors
|
|
||||||
import com.google.android.material.color.DynamicColorsOptions
|
|
||||||
import sh.sar.basedbank.R
|
|
||||||
|
|
||||||
object ThemeHelper {
|
|
||||||
|
|
||||||
const val PRESET_BLUE = "blue"
|
|
||||||
const val PRESET_RED = "red"
|
|
||||||
const val PRESET_GREEN = "green"
|
|
||||||
const val PRESET_CUSTOM = "custom"
|
|
||||||
|
|
||||||
fun isSystemTheme(context: Context): Boolean =
|
|
||||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("theme", "system") == "system"
|
|
||||||
|
|
||||||
fun getAccentPreset(context: Context): String =
|
|
||||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("accent_preset", PRESET_BLUE) ?: PRESET_BLUE
|
|
||||||
|
|
||||||
fun getCustomColor(context: Context): Int? {
|
|
||||||
val hex = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("accent_custom_color", null) ?: return null
|
|
||||||
return try { Color.parseColor(hex) } catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun presetSeedColor(context: Context, preset: String): Int = when (preset) {
|
|
||||||
PRESET_RED -> Color.parseColor("#D32F2F")
|
|
||||||
PRESET_GREEN -> Color.parseColor("#4CAF50")
|
|
||||||
PRESET_CUSTOM -> getCustomColor(context) ?: Color.parseColor("#3F65AD")
|
|
||||||
else -> Color.parseColor("#3F65AD")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPitchBlackEnabled(context: Context): Boolean =
|
|
||||||
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("pitch_black", false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the user-chosen accent theme to the given activity.
|
|
||||||
* Must be called BEFORE super.onCreate() so window-level attributes
|
|
||||||
* (status bar color, etc.) are resolved from the correct overlay.
|
|
||||||
* Has no effect when the theme is set to "system" (dynamic colors are
|
|
||||||
* handled by BasedBankApp via DynamicColors.applyToActivitiesIfAvailable).
|
|
||||||
*/
|
|
||||||
fun applyAccent(activity: AppCompatActivity) {
|
|
||||||
if (isSystemTheme(activity)) return
|
|
||||||
val preset = getAccentPreset(activity)
|
|
||||||
val seed = presetSeedColor(activity, preset)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
|
||||||
bitmap.setPixel(0, 0, seed)
|
|
||||||
DynamicColors.applyToActivityIfAvailable(
|
|
||||||
activity,
|
|
||||||
DynamicColorsOptions.Builder().setContentBasedSource(bitmap).build()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// API < 31: apply a simple style overlay (partial palette, but functional)
|
|
||||||
val styleRes = when (preset) {
|
|
||||||
PRESET_RED -> R.style.ThemeOverlay_Accent_Orange
|
|
||||||
PRESET_GREEN -> R.style.ThemeOverlay_Accent_Green
|
|
||||||
else -> R.style.ThemeOverlay_Accent_Blue
|
|
||||||
}
|
|
||||||
activity.theme.applyStyle(styleRes, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = activity.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
|
||||||
val isDark = prefs.getString("theme", "system") == "dark"
|
|
||||||
if (isDark && prefs.getBoolean("pitch_black", false)) {
|
|
||||||
activity.theme.applyStyle(R.style.ThemeOverlay_PitchBlack, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,20 +5,19 @@
|
|||||||
android:viewportWidth="60"
|
android:viewportWidth="60"
|
||||||
android:viewportHeight="60">
|
android:viewportHeight="60">
|
||||||
|
|
||||||
|
<!-- White background -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M0 59.776h59.785V0H0z"/>
|
||||||
|
|
||||||
<!-- Red background -->
|
<!-- Red background -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#E21B23"
|
android:fillColor="#E21B23"
|
||||||
android:pathData="M0 60h60V0H0z"/>
|
android:pathData="M3.297 56.421h53.191V3.356H3.298z"/>
|
||||||
|
|
||||||
<!-- White logo mark -->
|
<!-- White logo mark -->
|
||||||
<group
|
|
||||||
android:scaleX="0.85"
|
|
||||||
android:scaleY="0.85"
|
|
||||||
android:pivotX="30"
|
|
||||||
android:pivotY="30">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:pathData="M37.421 6.708v34.059h-3.7V20.853L22.763 40.767H18.65l18.77-34.06zM18.517 51.073l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.03c-.613.313-1.228.626-1.88.626-.65 0-1.265-.313-1.879-.626-.553-.283-1.108-.564-1.624-.564-.514 0-1.069.28-1.62.564-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.068.28-1.62.564c-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.069.28-1.622.564c-.614.313-1.228.626-1.879.626-.6 0-1.167-.265-1.73-.55v-1.446zm0-2.816l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.028c-.613.315-1.228.627-1.88.627-.65 0-1.265-.312-1.879-.627-.553-.281-1.108-.562-1.624-.562-.514 0-1.069.28-1.62.562-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.068.28-1.62.562c-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.069.28-1.622.562c-.614.315-1.228.627-1.879.627-.6 0-1.167-.264-1.73-.55v-1.445zm12.428-6.042h12.41v3.969h-12.41v-.001H18.531l-2.1-3.968h14.514z"/>
|
android:pathData="M37.421 6.708v34.059h-3.7V20.853L22.763 40.767H18.65l18.77-34.06zM18.517 51.073l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.03c-.613.313-1.228.626-1.88.626-.65 0-1.265-.313-1.879-.626-.553-.283-1.108-.564-1.624-.564-.514 0-1.069.28-1.62.564-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.068.28-1.62.564c-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.069.28-1.622.564c-.614.313-1.228.626-1.879.626-.6 0-1.167-.265-1.73-.55v-1.446zm0-2.816l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.028c-.613.315-1.228.627-1.88.627-.65 0-1.265-.312-1.879-.627-.553-.281-1.108-.562-1.624-.562-.514 0-1.069.28-1.62.562-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.068.28-1.62.562c-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.069.28-1.622.562c-.614.315-1.228.627-1.879.627-.6 0-1.167-.264-1.73-.55v-1.445zm12.428-6.042h12.41v3.969h-12.41v-.001H18.531l-2.1-3.968h14.514z"/>
|
||||||
</group>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<!-- Card icon -->
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
|
||||||
android:pathData="M20,4H4C2.89,4 2.01,4.89 2.01,6L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V6C22,4.89 21.11,4 20,4zM20,18H4v-6h16V18zM20,8H4V6h16V8z" />
|
|
||||||
<!-- Background circle separating gear from card -->
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorSurface"
|
|
||||||
android:pathData="M12.5,18 A5.5,5.5 0 1 0 23.5,18 A5.5,5.5 0 1 0 12.5,18 Z" />
|
|
||||||
<!--
|
|
||||||
Simple 6-tooth gear, scaled 0.5x and placed at (18,18) in the icon.
|
|
||||||
The gear path is drawn in a 24×24 space (center 12,12); the group
|
|
||||||
transform maps it: point(x,y) → (x·0.5+12, y·0.5+12).
|
|
||||||
Outer radius=10, root radius=7, hub radius=4.
|
|
||||||
Teeth are rectangular (straight-line) so they stay crisp at small scale.
|
|
||||||
Outer ring is clockwise; hub hole is counterclockwise (nonZero cutout).
|
|
||||||
-->
|
|
||||||
<group
|
|
||||||
android:scaleX="0.5"
|
|
||||||
android:scaleY="0.5"
|
|
||||||
android:translateX="12"
|
|
||||||
android:translateY="12">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
|
||||||
android:pathData="
|
|
||||||
M18.58,9.61 L21.85,10.26 L21.85,13.74 L18.58,14.39
|
|
||||||
L17.36,16.50 L18.43,19.66 L15.42,21.40 L13.22,18.89
|
|
||||||
L10.78,18.89 L8.58,21.40 L5.57,19.66 L6.64,16.50
|
|
||||||
L5.42,14.39 L2.15,13.74 L2.15,10.26 L5.42,9.61
|
|
||||||
L6.64,7.50 L5.57,4.34 L8.58,2.60 L10.78,5.11
|
|
||||||
L13.22,5.11 L15.42,2.60 L18.43,4.34 L17.36,7.50 Z
|
|
||||||
M12,8 A4,4 0 1 0 12,16 A4,4 0 1 0 12,8 Z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
android:fillColor="?attr/colorOnSurface"
|
||||||
android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z"/>
|
android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -14,47 +14,12 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<!-- Manage Card button row -->
|
<!-- Top spacer: pushes card to vertical center -->
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="end"
|
|
||||||
android:paddingHorizontal="12dp"
|
|
||||||
android:paddingTop="4dp">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnManageCard"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="0dp"
|
|
||||||
android:minHeight="0dp"
|
|
||||||
android:text="@string/card_manage"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:icon="@drawable/ic_manage_card"
|
|
||||||
app:iconSize="20dp"
|
|
||||||
app:iconPadding="4dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Top spacer: pushes card to vertical center (hidden in manage mode) -->
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/topSpacer"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<!-- Manage mode: selected card with overlays -->
|
|
||||||
<include
|
|
||||||
android:id="@+id/manageCardView"
|
|
||||||
layout="@layout/item_card_stack"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<!-- Horizontal card stack. Width/padding set programmatically for centering + peek. -->
|
<!-- Horizontal card stack. Width/padding set programmatically for centering + peek. -->
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvCards"
|
android:id="@+id/rvCards"
|
||||||
@@ -99,15 +64,14 @@
|
|||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:background="?attr/colorOutlineVariant" />
|
android:background="?attr/colorOutlineVariant" />
|
||||||
|
|
||||||
<!-- Primary pay actions (normal mode) -->
|
<!-- Primary pay actions -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/llPayButtons"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="12dp">
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnScanToPay"
|
android:id="@+id/btnScanToPay"
|
||||||
@@ -147,96 +111,56 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Default card toggle (manage mode only) -->
|
<!-- Secondary card management actions -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/llDefaultCardRow"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:paddingHorizontal="20dp"
|
|
||||||
android:paddingTop="4dp"
|
|
||||||
android:paddingBottom="4dp"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/card_set_as_default"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
|
||||||
android:id="@+id/switchDefaultCard"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Card management actions (manage mode only) -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/llManageButtons"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="8dp"
|
android:paddingHorizontal="8dp"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="4dp"
|
||||||
android:paddingBottom="12dp"
|
android:paddingBottom="8dp">
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnChangePin"
|
android:id="@+id/btnChangePin"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:minWidth="0dp"
|
android:minWidth="0dp"
|
||||||
android:minHeight="0dp"
|
android:minHeight="0dp"
|
||||||
android:paddingTop="14dp"
|
|
||||||
android:paddingBottom="14dp"
|
|
||||||
android:text="@string/card_action_change_pin"
|
android:text="@string/card_action_change_pin"
|
||||||
android:textSize="12sp"
|
android:textSize="11sp"
|
||||||
app:icon="@drawable/ic_edit"
|
app:icon="@drawable/ic_edit"
|
||||||
app:iconSize="22dp"
|
app:iconSize="16dp"
|
||||||
app:iconGravity="top"
|
app:iconPadding="3dp" />
|
||||||
app:iconPadding="6dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnFreeze"
|
android:id="@+id/btnFreeze"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:minWidth="0dp"
|
android:minWidth="0dp"
|
||||||
android:minHeight="0dp"
|
android:minHeight="0dp"
|
||||||
android:paddingTop="14dp"
|
|
||||||
android:paddingBottom="14dp"
|
|
||||||
android:text="@string/card_action_freeze"
|
android:text="@string/card_action_freeze"
|
||||||
android:textSize="12sp"
|
android:textSize="11sp"
|
||||||
app:icon="@drawable/ic_freeze"
|
app:icon="@drawable/ic_freeze"
|
||||||
app:iconSize="22dp"
|
app:iconSize="16dp"
|
||||||
app:iconGravity="top"
|
app:iconPadding="3dp" />
|
||||||
app:iconPadding="6dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnBlock"
|
android:id="@+id/btnBlock"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:minWidth="0dp"
|
android:minWidth="0dp"
|
||||||
android:minHeight="0dp"
|
android:minHeight="0dp"
|
||||||
android:paddingTop="14dp"
|
|
||||||
android:paddingBottom="14dp"
|
|
||||||
android:text="@string/card_action_block"
|
android:text="@string/card_action_block"
|
||||||
android:textSize="12sp"
|
android:textSize="11sp"
|
||||||
app:icon="@drawable/ic_block"
|
app:icon="@drawable/ic_block"
|
||||||
app:iconSize="22dp"
|
app:iconSize="16dp"
|
||||||
app:iconGravity="top"
|
app:iconPadding="3dp" />
|
||||||
app:iconPadding="6dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -116,8 +116,6 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||||
app:cardCornerRadius="12dp"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardElevation="0dp">
|
app:cardElevation="0dp">
|
||||||
|
|||||||
@@ -176,9 +176,9 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Blocked funds row: MVR + USD separate cards (hidden when no blocked amounts) -->
|
<!-- Attention row: Blocked + Overdue (hidden when nothing applies) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/rowBlocked"
|
android:id="@+id/rowAttention"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/cardBlockedMvr"
|
android:id="@+id/cardBlocked"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -205,12 +205,12 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/dashboard_blocked_mvr"
|
android:text="@string/dashboard_blocked"
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
android:textColor="?attr/colorOnErrorContainer" />
|
android:textColor="?attr/colorOnErrorContainer" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvBlockedMvr"
|
android:id="@+id/tvBlockedTotal"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
@@ -218,12 +218,22 @@
|
|||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
android:textColor="?attr/colorOnErrorContainer" />
|
android:textColor="?attr/colorOnErrorContainer" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBlockedSecondary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnErrorContainer"
|
||||||
|
android:alpha="0.75"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/cardBlockedUsd"
|
android:id="@+id/cardOverdue"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -231,52 +241,6 @@
|
|||||||
app:cardElevation="1dp"
|
app:cardElevation="1dp"
|
||||||
app:cardCornerRadius="12dp"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardBackgroundColor="?attr/colorErrorContainer"
|
app:cardBackgroundColor="?attr/colorErrorContainer"
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/dashboard_blocked_usd"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
|
||||||
android:textColor="?attr/colorOnErrorContainer" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvBlockedUsd"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:text="USD —"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
|
||||||
android:textColor="?attr/colorOnErrorContainer" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Overdue row (hidden when no overdue financing) -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/rowAttention"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:id="@+id/cardOverdue"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardElevation="1dp"
|
|
||||||
app:cardCornerRadius="12dp"
|
|
||||||
app:cardBackgroundColor="?attr/colorErrorContainer"
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
@@ -361,6 +325,14 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/nav_pay_with_card"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvCards"
|
android:id="@+id/rvCards"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -386,6 +358,14 @@
|
|||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="16dp">
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dashboard_quick_actions"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:hint="@string/paymvqr_amount_hint"
|
android:hint="@string/paymvqr_amount_hint"
|
||||||
|
app:helperText="@string/paymvqr_amount_helper"
|
||||||
app:prefixText="MVR ">
|
app:prefixText="MVR ">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
@@ -75,47 +76,6 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<!-- Reference / purpose (optional) -->
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/tilReference"
|
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:hint="@string/paymvqr_reference_hint">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/etReference"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<!-- Include phone number toggle -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_marginBottom="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/paymvqr_include_phone"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
|
||||||
android:id="@+id/switchIncludePhone"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:checked="true" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Action buttons — always visible; share/save disabled until QR is rendered -->
|
<!-- Action buttons — always visible; share/save disabled until QR is rendered -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layoutActions"
|
android:id="@+id/layoutActions"
|
||||||
|
|||||||
@@ -156,87 +156,6 @@
|
|||||||
|
|
||||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
<!-- Pitch black — enabled only in explicit dark mode -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/rowPitchBlack"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/settings_pitch_black"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
|
||||||
android:id="@+id/switchPitchBlack"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Accent color — always shown, disabled/grayed in system theme mode -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/sectionAccentColor"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_marginTop="24dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/settings_accent_color"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
|
||||||
android:layout_marginBottom="12dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
|
||||||
android:id="@+id/accentToggle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:singleSelection="true"
|
|
||||||
app:selectionRequired="true">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnAccentBlue"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/accent_blue" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnAccentOrange"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/accent_orange" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnAccentGreen"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/accent_green" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnAccentCustom"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/accent_custom" />
|
|
||||||
|
|
||||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -194,6 +194,44 @@
|
|||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowHideAmounts"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_hide_amounts"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_hide_amounts_desc"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchHideAmounts"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/rowBlockScreenshots"
|
android:id="@+id/rowBlockScreenshots"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="320dp"
|
android:layout_width="280dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
app:cardElevation="2dp">
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
|||||||
@@ -115,13 +115,11 @@
|
|||||||
<!-- PayMV QR Generator -->
|
<!-- PayMV QR Generator -->
|
||||||
<string name="paymvqr_select_account">Select account</string>
|
<string name="paymvqr_select_account">Select account</string>
|
||||||
<string name="paymvqr_amount_hint">Amount (optional)</string>
|
<string name="paymvqr_amount_hint">Amount (optional)</string>
|
||||||
|
<string name="paymvqr_amount_helper">Leave empty to allow payer to enter any amount</string>
|
||||||
<string name="paymvqr_share">Share</string>
|
<string name="paymvqr_share">Share</string>
|
||||||
<string name="paymvqr_save_image">Save Image</string>
|
<string name="paymvqr_save_image">Save Image</string>
|
||||||
<string name="paymvqr_saved">QR saved to gallery</string>
|
<string name="paymvqr_saved">QR saved to gallery</string>
|
||||||
<string name="paymvqr_save_failed">Failed to save image</string>
|
<string name="paymvqr_save_failed">Failed to save image</string>
|
||||||
<string name="paymvqr_include_phone">Include phone number</string>
|
|
||||||
<string name="paymvqr_reference_hint">Reference (optional)</string>
|
|
||||||
<string name="paymvqr_reference_default">PayMV QR Transfer</string>
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<string name="action_lock">Lock app</string>
|
<string name="action_lock">Lock app</string>
|
||||||
@@ -150,15 +148,6 @@
|
|||||||
<string name="theme_system">System</string>
|
<string name="theme_system">System</string>
|
||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
<string name="settings_pitch_black">Pitch Black</string>
|
|
||||||
<string name="settings_accent_color">Accent Color</string>
|
|
||||||
<string name="accent_blue">Blue</string>
|
|
||||||
<string name="accent_orange">Red</string>
|
|
||||||
<string name="accent_green">Green</string>
|
|
||||||
<string name="accent_custom">Custom</string>
|
|
||||||
<string name="accent_custom_pick">Custom Accent Color</string>
|
|
||||||
<string name="accent_custom_hint">#RRGGBB hex color</string>
|
|
||||||
<string name="accent_invalid_color">Invalid color — enter a valid hex code</string>
|
|
||||||
<string name="language">Language</string>
|
<string name="language">Language</string>
|
||||||
<string name="lang_english">English</string>
|
<string name="lang_english">English</string>
|
||||||
<string name="lang_dhivehi">ދިވެހި</string>
|
<string name="lang_dhivehi">ދިވެހި</string>
|
||||||
@@ -220,8 +209,7 @@
|
|||||||
<string name="cards">Cards</string>
|
<string name="cards">Cards</string>
|
||||||
<string name="available_balance">Available Balance</string>
|
<string name="available_balance">Available Balance</string>
|
||||||
<string name="account_blocked_label">%1$s blocked</string>
|
<string name="account_blocked_label">%1$s blocked</string>
|
||||||
<string name="dashboard_blocked_mvr">Blocked MVR</string>
|
<string name="dashboard_blocked">Blocked Funds</string>
|
||||||
<string name="dashboard_blocked_usd">Blocked USD</string>
|
|
||||||
<string name="dashboard_overdue">Overdue Financing</string>
|
<string name="dashboard_overdue">Overdue Financing</string>
|
||||||
|
|
||||||
<!-- Transfer -->
|
<!-- Transfer -->
|
||||||
@@ -243,7 +231,6 @@
|
|||||||
<string name="transfer_qr_invalid">Invalid or unsupported QR code</string>
|
<string name="transfer_qr_invalid">Invalid or unsupported QR code</string>
|
||||||
<string name="qr_camera_permission_title">Camera permission required</string>
|
<string name="qr_camera_permission_title">Camera permission required</string>
|
||||||
<string name="qr_camera_permission_message">Camera access is needed to scan QR codes. Please grant the permission in Settings.</string>
|
<string name="qr_camera_permission_message">Camera access is needed to scan QR codes. Please grant the permission in Settings.</string>
|
||||||
<string name="camera_permission_profile_message">Camera access is needed to take a photo. Please grant the permission in Settings.</string>
|
|
||||||
<string name="go_to_settings">Go to Settings</string>
|
<string name="go_to_settings">Go to Settings</string>
|
||||||
<string name="transfer_select_source_first">Select a source account first</string>
|
<string name="transfer_select_source_first">Select a source account first</string>
|
||||||
<string name="transfer_enter_account_first">Enter an account number first</string>
|
<string name="transfer_enter_account_first">Enter an account number first</string>
|
||||||
@@ -330,8 +317,6 @@
|
|||||||
<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="card_manage">Manage Card</string>
|
|
||||||
<string name="card_set_as_default">Set as Default Card</string>
|
|
||||||
<string name="card_action_change_pin">Change PIN</string>
|
<string name="card_action_change_pin">Change PIN</string>
|
||||||
<string name="card_action_freeze">Freeze</string>
|
<string name="card_action_freeze">Freeze</string>
|
||||||
<string name="card_action_block">Block</string>
|
<string name="card_action_block">Block</string>
|
||||||
|
|||||||
@@ -6,32 +6,4 @@
|
|||||||
<item name="colorSecondary">@color/seed_secondary</item>
|
<item name="colorSecondary">@color/seed_secondary</item>
|
||||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Accent overlays — applied on API < 31 as fallback when content-based dynamic colors unavailable -->
|
|
||||||
<style name="ThemeOverlay_Accent_Blue" parent="">
|
|
||||||
<item name="colorPrimary">#3F65AD</item>
|
|
||||||
<item name="colorSecondary">#9AD141</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="ThemeOverlay_Accent_Orange" parent="">
|
|
||||||
<item name="colorPrimary">#D32F2F</item>
|
|
||||||
<item name="colorSecondary">#EF9A9A</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="ThemeOverlay_Accent_Green" parent="">
|
|
||||||
<item name="colorPrimary">#4CAF50</item>
|
|
||||||
<item name="colorSecondary">#80CBC4</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Pitch black overlay — forces pure black surfaces for OLED displays -->
|
|
||||||
<style name="ThemeOverlay_PitchBlack" parent="">
|
|
||||||
<item name="android:colorBackground">#000000</item>
|
|
||||||
<item name="colorSurface">#000000</item>
|
|
||||||
<item name="colorSurfaceVariant">#0d0d0d</item>
|
|
||||||
<item name="colorSurfaceContainer">#0d0d0d</item>
|
|
||||||
<item name="colorSurfaceContainerLow">#000000</item>
|
|
||||||
<item name="colorSurfaceContainerLowest">#000000</item>
|
|
||||||
<item name="colorSurfaceContainerHigh">#1a1a1a</item>
|
|
||||||
<item name="colorSurfaceContainerHighest">#1a1a1a</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user