forked from shihaam/thijooree
fahipay serialized multi-login added
This commit is contained in:
@@ -17,14 +17,42 @@ class BasedBankApp : Application() {
|
|||||||
// Held in memory after successful login; cleared on logout
|
// Held in memory after successful login; cleared on logout
|
||||||
var accounts: List<MibAccount> = emptyList()
|
var accounts: List<MibAccount> = emptyList()
|
||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
var mibSession: MibSession? = null
|
/** Active MIB sessions keyed by loginId (= MIB username). */
|
||||||
var mibProfiles: List<MibProfile> = emptyList()
|
val mibSessions: MutableMap<String, MibSession> = mutableMapOf()
|
||||||
|
val mibProfilesMap: MutableMap<String, List<MibProfile>> = mutableMapOf()
|
||||||
|
val mibLoginFlows: MutableMap<String, MibLoginFlow> = mutableMapOf()
|
||||||
|
var mibAccounts: List<MibAccount> = emptyList()
|
||||||
/** Active BML sessions keyed by loginId (= BML username). */
|
/** Active BML sessions keyed by loginId (= BML username). */
|
||||||
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
||||||
var bmlAccounts: List<MibAccount> = emptyList()
|
var bmlAccounts: List<MibAccount> = emptyList()
|
||||||
var fahipaySession: FahipaySession? = null
|
/** Active Fahipay sessions keyed by loginId (= profileId). */
|
||||||
|
val fahipaySessions: MutableMap<String, FahipaySession> = mutableMapOf()
|
||||||
var fahipayAccounts: List<MibAccount> = emptyList()
|
var fahipayAccounts: List<MibAccount> = emptyList()
|
||||||
|
|
||||||
|
/** Returns the MIB session for the given account (matched via loginTag). */
|
||||||
|
fun mibSessionFor(account: MibAccount): MibSession? =
|
||||||
|
mibSessions[account.loginTag.removePrefix("mib_")]
|
||||||
|
|
||||||
|
/** Returns any available MIB session. */
|
||||||
|
fun anyMibSession(): MibSession? = mibSessions.values.firstOrNull()
|
||||||
|
|
||||||
|
/** Returns all MIB profiles across all logins. */
|
||||||
|
fun allMibProfiles(): List<MibProfile> = mibProfilesMap.values.flatten()
|
||||||
|
|
||||||
|
/** Returns the MibLoginFlow for a given loginId, creating and caching it if needed. */
|
||||||
|
fun mibFlowFor(loginId: String): MibLoginFlow =
|
||||||
|
mibLoginFlows.getOrPut(loginId) {
|
||||||
|
MibLoginFlow(CredentialStore(this)).also { flow ->
|
||||||
|
flow.onSessionRefreshed = { session, profiles ->
|
||||||
|
mibSessions[loginId] = session
|
||||||
|
mibProfilesMap[loginId] = profiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns any available MibLoginFlow. */
|
||||||
|
fun anyMibFlow(): MibLoginFlow? = mibLoginFlows.values.firstOrNull()
|
||||||
|
|
||||||
/** Returns the BML session for the given account (matched via loginTag). */
|
/** Returns the BML session for the given account (matched via loginTag). */
|
||||||
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
||||||
bmlSessions[account.loginTag.removePrefix("bml_")]
|
bmlSessions[account.loginTag.removePrefix("bml_")]
|
||||||
@@ -32,18 +60,13 @@ class BasedBankApp : Application() {
|
|||||||
/** Returns any available BML session (for non-account-specific operations). */
|
/** Returns any available BML session (for non-account-specific operations). */
|
||||||
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
||||||
|
|
||||||
|
/** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */
|
||||||
|
fun fahipaySessionFor(account: MibAccount): FahipaySession? =
|
||||||
|
fahipaySessions[account.loginTag.removePrefix("fahipay_")]
|
||||||
|
|
||||||
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
||||||
val mibMutex = Mutex()
|
val mibMutex = Mutex()
|
||||||
|
|
||||||
val mibLoginFlow by lazy {
|
|
||||||
MibLoginFlow(CredentialStore(this)).also { flow ->
|
|
||||||
flow.onSessionRefreshed = { session, profiles ->
|
|
||||||
mibSession = session
|
|
||||||
mibProfiles = profiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
|
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
|
||||||
|
|
||||||
// Stored after login so the session can be silently recovered on 419
|
// Stored after login so the session can be silently recovered on 419
|
||||||
|
@Volatile private var loginId: String = ""
|
||||||
@Volatile private var storedUsername: String? = null
|
@Volatile private var storedUsername: String? = null
|
||||||
@Volatile private var storedPasswordHash: String? = null
|
@Volatile private var storedPasswordHash: String? = null
|
||||||
@Volatile private var storedOtpSeed: String? = null
|
@Volatile private var storedOtpSeed: String? = null
|
||||||
@@ -58,11 +59,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
* Returns list of accounts from all profiles on success.
|
* Returns list of accounts from all profiles on success.
|
||||||
*/
|
*/
|
||||||
fun login(username: String, passwordHash: String, otpSeed: String): List<MibAccount> {
|
fun login(username: String, passwordHash: String, otpSeed: String): List<MibAccount> {
|
||||||
|
loginId = username
|
||||||
storedUsername = username
|
storedUsername = username
|
||||||
storedPasswordHash = passwordHash
|
storedPasswordHash = passwordHash
|
||||||
storedOtpSeed = otpSeed
|
storedOtpSeed = otpSeed
|
||||||
val appId = getOrCreateAppId()
|
val appId = getOrCreateAppId()
|
||||||
val keys = credentialStore.loadMibKeys()
|
val keys = credentialStore.loadMibKeys(loginId)
|
||||||
|
|
||||||
return if (keys != null) {
|
return if (keys != null) {
|
||||||
regularLogin(username, passwordHash, appId, keys.first, keys.second)
|
regularLogin(username, passwordHash, appId, keys.first, keys.second)
|
||||||
@@ -106,7 +108,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
val keyData = otpResp.getJSONArray("data").getJSONObject(0)
|
val keyData = otpResp.getJSONArray("data").getJSONObject(0)
|
||||||
val key1 = keyData.getString("key1")
|
val key1 = keyData.getString("key1")
|
||||||
val key2 = keyData.getString("key2")
|
val key2 = keyData.getString("key2")
|
||||||
credentialStore.saveMibKeys(key1, key2)
|
credentialStore.saveMibKeys(loginId, key1, key2)
|
||||||
|
|
||||||
return regularLogin(username, passwordHash, appId, key1, key2)
|
return regularLogin(username, passwordHash, appId, key1, key2)
|
||||||
}
|
}
|
||||||
@@ -139,7 +141,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
lastSession = session2
|
lastSession = session2
|
||||||
lastProfiles = profiles // keep ALL profiles so settings can show them all
|
lastProfiles = profiles // keep ALL profiles so settings can show them all
|
||||||
|
|
||||||
val hidden = credentialStore.getHiddenMibProfileIds()
|
val hidden = credentialStore.getHiddenMibProfileIds(loginId)
|
||||||
|
|
||||||
// When the server already selected the profile and returned balances in A41
|
// When the server already selected the profile and returned balances in A41
|
||||||
// (single-profile case: profileSelected=true), use those accounts directly
|
// (single-profile case: profileSelected=true), use those accounts directly
|
||||||
@@ -433,11 +435,11 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
private fun generateOtp(seed: String): String = Totp.generate(seed)
|
private fun generateOtp(seed: String): String = Totp.generate(seed)
|
||||||
|
|
||||||
private fun getOrCreateAppId(): String {
|
private fun getOrCreateAppId(): String {
|
||||||
var id = credentialStore.loadMibAppId()
|
var id = credentialStore.loadMibAppId(loginId)
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("")
|
id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("")
|
||||||
credentialStore.saveMibAppId(id)
|
credentialStore.saveMibAppId(loginId, id)
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val label: String,
|
val label: String,
|
||||||
val isBml: Boolean,
|
val isBml: Boolean,
|
||||||
val mibProfile: MibProfile? = null,
|
val mibProfile: MibProfile? = null,
|
||||||
|
val mibLoginId: String? = null,
|
||||||
val bmlLoginId: String? = null,
|
val bmlLoginId: String? = null,
|
||||||
val subtitle: String = ""
|
val subtitle: String = ""
|
||||||
)
|
)
|
||||||
@@ -91,8 +92,10 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private fun buildDestinations(): List<DestinationOption> {
|
private fun buildDestinations(): List<DestinationOption> {
|
||||||
val list = mutableListOf<DestinationOption>()
|
val list = mutableListOf<DestinationOption>()
|
||||||
for (profile in app.mibProfiles) {
|
for ((loginId, profiles) in app.mibProfilesMap) {
|
||||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, subtitle = profile.cifType))
|
for (profile in profiles) {
|
||||||
|
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, mibLoginId = loginId, subtitle = profile.cifType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val store = CredentialStore(requireContext())
|
val store = CredentialStore(requireContext())
|
||||||
for ((loginId, _) in app.bmlSessions) {
|
for ((loginId, _) in app.bmlSessions) {
|
||||||
@@ -249,7 +252,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (mibVerified != null) return mibVerified
|
if (mibVerified != null) return mibVerified
|
||||||
|
|
||||||
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
||||||
val mibSess = app.mibSession ?: return null
|
val mibSess = app.anyMibSession() ?: return null
|
||||||
return try {
|
return try {
|
||||||
val info = MibTransferClient().lookup(mibSess, input)
|
val info = MibTransferClient().lookup(mibSess, input)
|
||||||
BmlAccountValidation(
|
BmlAccountValidation(
|
||||||
@@ -266,11 +269,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? {
|
private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? {
|
||||||
val mibSess = app.mibSession ?: return null
|
val loginId = dest.mibLoginId ?: return null
|
||||||
|
val mibSess = app.mibSessions[loginId] ?: return null
|
||||||
val profile = dest.mibProfile ?: return null
|
val profile = dest.mibProfile ?: return null
|
||||||
|
|
||||||
val mibResult = try {
|
val mibResult = try {
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
app.mibFlowFor(loginId).switchProfile(mibSess, profile)
|
||||||
val info = MibTransferClient().lookup(mibSess, input)
|
val info = MibTransferClient().lookup(mibSess, input)
|
||||||
BmlAccountValidation(
|
BmlAccountValidation(
|
||||||
trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL",
|
trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL",
|
||||||
@@ -429,8 +433,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveToMib(alias: String): Boolean {
|
private fun saveToMib(alias: String): Boolean {
|
||||||
val mibSess = app.mibSession ?: return false
|
|
||||||
val dest = selectedDest ?: return false
|
val dest = selectedDest ?: return false
|
||||||
|
val loginId = dest.mibLoginId ?: return false
|
||||||
|
val mibSess = app.mibSessions[loginId] ?: return false
|
||||||
val profile = dest.mibProfile ?: return false
|
val profile = dest.mibProfile ?: return false
|
||||||
val account = mibLookupAccount ?: return false
|
val account = mibLookupAccount ?: return false
|
||||||
val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR"
|
val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR"
|
||||||
@@ -442,7 +447,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val name = bmlLookup?.name ?: ""
|
val name = bmlLookup?.name ?: ""
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
app.mibFlowFor(loginId).switchProfile(mibSess, profile)
|
||||||
MibContactsClient().createContact(
|
MibContactsClient().createContact(
|
||||||
session = mibSess,
|
session = mibSess,
|
||||||
benefType = benefType,
|
benefType = benefType,
|
||||||
@@ -488,8 +493,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
|
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
|
||||||
} else {
|
} else {
|
||||||
val profile = dest.mibProfile ?: return@launch
|
val profile = dest.mibProfile ?: return@launch
|
||||||
val mibSess = app.mibSession ?: return@launch
|
val mibLoginId = dest.mibLoginId ?: return@launch
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
val mibSess = app.mibSessions[mibLoginId] ?: return@launch
|
||||||
|
app.mibFlowFor(mibLoginId).switchProfile(mibSess, profile)
|
||||||
val fresh = MibContactsClient().fetchContacts(mibSess)
|
val fresh = MibContactsClient().fetchContacts(mibSess)
|
||||||
.map { it.copy(profileId = profile.profileId) }
|
.map { it.copy(profileId = profile.profileId) }
|
||||||
val existing = viewModel.contacts.value ?: emptyList()
|
val existing = viewModel.contacts.value ?: emptyList()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
||||||
private val profileImageHashes = mutableSetOf<String>()
|
private val profileImageHashes = mutableSetOf<String>()
|
||||||
private val app get() = requireActivity().application as BasedBankApp
|
private val app get() = requireActivity().application as BasedBankApp
|
||||||
private val session get() = app.mibSession
|
private val session get() = app.anyMibSession()
|
||||||
|
|
||||||
private var fromAccountNumber: String = ""
|
private var fromAccountNumber: String = ""
|
||||||
private var mediator: TabLayoutMediator? = null
|
private var mediator: TabLayoutMediator? = null
|
||||||
@@ -291,7 +291,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (hash in profileImageHashes) {
|
val base64 = if (hash in profileImageHashes) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ContactsFragment : Fragment() {
|
|||||||
private val pendingHashes = mutableSetOf<String>()
|
private val pendingHashes = mutableSetOf<String>()
|
||||||
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
||||||
private val app get() = requireActivity().application as BasedBankApp
|
private val app get() = requireActivity().application as BasedBankApp
|
||||||
private val session get() = app.mibSession
|
private val session get() = app.anyMibSession()
|
||||||
|
|
||||||
private var allContacts: List<ContactDisplay> = emptyList()
|
private var allContacts: List<ContactDisplay> = emptyList()
|
||||||
private var currentSearch: String = ""
|
private var currentSearch: String = ""
|
||||||
|
|||||||
@@ -128,31 +128,36 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
||||||
// Came from fresh manual login — accounts ready, rest fetched in background
|
// Came from fresh manual login — accounts ready, rest fetched in background
|
||||||
val mibAccounts = app.accounts.filter { it.bank == "MIB" }
|
val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
||||||
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
|
||||||
viewModel.accounts.value = merged.filterVisibleAccounts()
|
viewModel.accounts.value = merged.filterVisibleAccounts()
|
||||||
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
if (app.mibAccounts.isNotEmpty()) AccountCache.save(this, app.mibAccounts)
|
||||||
if (app.bmlAccounts.isNotEmpty()) {
|
if (app.bmlAccounts.isNotEmpty()) {
|
||||||
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
||||||
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
|
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
|
||||||
}
|
}
|
||||||
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
|
if (app.fahipayAccounts.isNotEmpty()) {
|
||||||
|
val byLoginId = app.fahipayAccounts.groupBy { it.loginTag.removePrefix("fahipay_") }
|
||||||
|
byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) }
|
||||||
|
}
|
||||||
|
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val cachedMib = AccountCache.load(this)
|
val cachedMib = AccountCache.load(this)
|
||||||
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
|
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
|
||||||
val cachedFahipay = AccountCache.loadFahipay(this)
|
val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds())
|
||||||
val merged = cachedMib + cachedBml + cachedFahipay
|
val merged = cachedMib + cachedBml + cachedFahipay
|
||||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
@@ -160,7 +165,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
autoRefresh(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dashboard on first create
|
// Show dashboard on first create
|
||||||
@@ -169,27 +174,29 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep MIB session alive every 25 seconds while the app is in the foreground
|
// Keep all MIB sessions alive every 25 seconds while the app is in the foreground
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(25_000)
|
delay(25_000)
|
||||||
val session = (application as BasedBankApp).mibSession ?: continue
|
val sessions = (application as BasedBankApp).mibSessions.values.toList()
|
||||||
withContext(Dispatchers.IO) {
|
for (session in sessions) {
|
||||||
try {
|
withContext(Dispatchers.IO) {
|
||||||
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
try {
|
||||||
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
||||||
val request = Request.Builder()
|
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||||
.url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive")
|
val request = Request.Builder()
|
||||||
.post(ByteArray(0).toRequestBody())
|
.url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive")
|
||||||
.header("Cookie", cookieHeader)
|
.post(ByteArray(0).toRequestBody())
|
||||||
.header("User-Agent", "okhttp/4.11.0")
|
.header("Cookie", cookieHeader)
|
||||||
.header("Accept", "application/json, text/plain, */*")
|
.header("User-Agent", "okhttp/4.11.0")
|
||||||
.header("Accept-Encoding", "gzip")
|
.header("Accept", "application/json, text/plain, */*")
|
||||||
.header("Connection", "Keep-Alive")
|
.header("Accept-Encoding", "gzip")
|
||||||
.build()
|
.header("Connection", "Keep-Alive")
|
||||||
OkHttpClient().newCall(request).execute().close()
|
.build()
|
||||||
} catch (_: Exception) {}
|
OkHttpClient().newCall(request).execute().close()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,51 +360,47 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
fun relogin() {
|
fun relogin() {
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val hasMib = store.hasMibCredentials()
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
if (!hasMib && bmlLoginIds.isEmpty() && !hasFahipay) {
|
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) {
|
||||||
startActivity(Intent(this, LoginActivity::class.java))
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Immediately drop accounts for logged-out banks from the displayed list
|
// Immediately drop accounts for logged-out logins from the displayed list
|
||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
viewModel.accounts.value = current.filter { acc ->
|
viewModel.accounts.value = current.filter { acc ->
|
||||||
if (!hasMib && acc.bank == "MIB") return@filter false
|
if (acc.bank == "MIB") return@filter acc.loginTag.removePrefix("mib_") in mibLoginIds
|
||||||
if (acc.bank == "BML") {
|
if (acc.bank == "BML") return@filter acc.loginTag.removePrefix("bml_") in bmlLoginIds
|
||||||
val loginId = acc.loginTag.removePrefix("bml_")
|
if (acc.bank == "FAHIPAY") return@filter acc.loginTag.removePrefix("fahipay_") in fahipayLoginIds
|
||||||
return@filter loginId in bmlLoginIds
|
|
||||||
}
|
|
||||||
if (!hasFahipay && acc.bank == "FAHIPAY") return@filter false
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
autoRefresh(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefresh(
|
private fun autoRefresh(store: CredentialStore) {
|
||||||
mibCreds: CredentialStore.MibCredentials?,
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
fahipayCreds: CredentialStore.FahipayCredentials?,
|
|
||||||
store: CredentialStore
|
|
||||||
) {
|
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
if (mibCreds == null && bmlLoginIds.isEmpty() && fahipayCreds == null) return
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
|
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val mibJob = mibCreds?.let {
|
// One async job per MIB login, all run in parallel
|
||||||
async(Dispatchers.IO) {
|
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||||
|
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||||
|
loginId to async(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
||||||
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
|
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.accounts = accounts
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
app.mibSession = flow.lastSession
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.mibLoginFlows[loginId] = flow
|
||||||
AccountCache.save(this@HomeActivity, accounts)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
CredentialStore(this@HomeActivity).saveMibProfiles(flow.lastProfiles)
|
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
|
} catch (_: Exception) { AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,78 +438,87 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val fahipayJob = fahipayCreds?.let { creds ->
|
// One async job per Fahipay login, all run in parallel
|
||||||
async(Dispatchers.IO) {
|
val fahipayJobs = fahipayLoginIds.mapNotNull { loginId ->
|
||||||
|
val creds = store.loadFahipayCredentials(loginId) ?: return@mapNotNull null
|
||||||
|
loginId to async(Dispatchers.IO) {
|
||||||
val fahipayFlow = FahipayLoginFlow()
|
val fahipayFlow = FahipayLoginFlow()
|
||||||
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
||||||
|
val loginTag = "fahipay_$loginId"
|
||||||
|
|
||||||
val savedSession = store.loadFahipaySession()
|
val savedSession = store.loadFahipaySession(loginId)
|
||||||
if (savedSession != null) {
|
if (savedSession != null) {
|
||||||
try {
|
try {
|
||||||
val session = FahipaySession(savedSession.first, savedSession.second)
|
val session = FahipaySession(savedSession.first, savedSession.second)
|
||||||
fahipayFlow.setSessionCookie(session.sessionCookie)
|
fahipayFlow.setSessionCookie(session.sessionCookie)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val balance = fahipayFlow.fetchBalance(session)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val profile = fahipayFlow.fetchProfile(session)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
return@async accounts
|
||||||
return@async Pair(session, accounts)
|
} catch (_: Exception) { }
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
||||||
if (step.twoFactorRequired) {
|
if (step.twoFactorRequired) {
|
||||||
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
return@async AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val authId = step.authId ?: return@async AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
||||||
val session = FahipaySession(authId, cookieValue)
|
val session = FahipaySession(authId, cookieValue)
|
||||||
store.saveFahipaySession(authId, cookieValue)
|
store.saveFahipaySession(loginId, authId, cookieValue)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val profile = fahipayFlow.fetchProfile(session)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val balance = fahipayFlow.fetchBalance(session)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
accounts
|
||||||
Pair(session, accounts)
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
val mibResults = mibJobs.map { (loginId, job) -> loginId to job.await() }
|
||||||
|
val mibAccounts = mibResults.flatMap { it.second }
|
||||||
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
||||||
val bmlAccounts = bmlResults.flatMap { it.second }
|
val bmlAccounts = bmlResults.flatMap { it.second }
|
||||||
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val fahipayAccounts = fahipayJobs.flatMap { (_, job) -> job.await() }
|
||||||
|
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
|
app.mibAccounts = mibAccounts
|
||||||
|
AccountCache.save(this@HomeActivity, mibAccounts)
|
||||||
app.bmlAccounts = bmlAccounts
|
app.bmlAccounts = bmlAccounts
|
||||||
|
app.fahipayAccounts = fahipayAccounts
|
||||||
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filters MIB accounts whose profileId the user has hidden in settings. */
|
/** Filters MIB accounts whose profileId the user has hidden in settings. */
|
||||||
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
||||||
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds()
|
val store = CredentialStore(this@HomeActivity)
|
||||||
if (hidden.isEmpty()) return this
|
return filter { acc ->
|
||||||
return filter { it.bank != "MIB" || it.profileId !in hidden }
|
if (acc.bank != "MIB") return@filter true
|
||||||
|
val loginId = acc.loginTag.removePrefix("mib_")
|
||||||
|
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||||
|
hidden.isEmpty() || acc.profileId !in hidden
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filters MIB profiles the user has hidden — prevents API requests for hidden profiles. */
|
/** Filters MIB profiles the user has hidden for a given loginId. */
|
||||||
private fun List<MibProfile>.filterVisibleProfiles(): List<MibProfile> {
|
private fun List<MibProfile>.filterVisibleProfiles(loginId: String): List<MibProfile> {
|
||||||
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds()
|
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds(loginId)
|
||||||
if (hidden.isEmpty()) return this
|
if (hidden.isEmpty()) return this
|
||||||
return filter { it.profileId !in hidden }
|
return filter { it.profileId !in hidden }
|
||||||
}
|
}
|
||||||
@@ -571,9 +583,12 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
|
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
|
||||||
}
|
}
|
||||||
// Refresh all banks in background
|
// Refresh all banks in background
|
||||||
refreshContacts(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshContacts(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
refreshBmlContacts(app)
|
refreshBmlContacts(app)
|
||||||
if (app.fahipaySession != null) refreshFahipayContacts(app.fahipaySession!!)
|
for ((_, session) in app.fahipaySessions) refreshFahipayContacts(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFahipayContacts(session: FahipaySession) {
|
private fun refreshFahipayContacts(session: FahipaySession) {
|
||||||
@@ -609,9 +624,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
|
private fun refreshContacts(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = MibLoginFlow(CredentialStore(this))
|
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||||
val contactsClient = MibContactsClient()
|
val contactsClient = MibContactsClient()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -636,8 +651,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
if (allContacts.isNotEmpty()) {
|
if (allContacts.isNotEmpty()) {
|
||||||
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
||||||
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
|
||||||
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
|
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds())
|
||||||
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
||||||
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
||||||
@@ -653,7 +668,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
if (src.bank == "FAHIPAY") {
|
if (src.bank == "FAHIPAY") {
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.fahipaySession ?: return@withContext null
|
val sess = app.fahipaySessionFor(src) ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val flow = FahipayLoginFlow()
|
||||||
flow.setSessionCookie(sess.sessionCookie)
|
flow.setSessionCookie(sess.sessionCookie)
|
||||||
@@ -661,8 +676,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val profile = flow.fetchProfile(sess)
|
val profile = flow.fetchProfile(sess)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
val loginTag = "fahipay_${profile.profileId}"
|
||||||
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
val loginId = src.loginTag.removePrefix("fahipay_")
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
@@ -683,40 +699,43 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val otherAccounts = current.filter { it.bank != "BML" || it.loginTag != src.loginTag }
|
val otherAccounts = current.filter { it.bank != "BML" || it.loginTag != src.loginTag }
|
||||||
viewModel.accounts.postValue(otherAccounts + fresh)
|
viewModel.accounts.postValue(otherAccounts + fresh)
|
||||||
} else {
|
} else {
|
||||||
|
val loginId = src.loginTag.removePrefix("mib_")
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val store = CredentialStore(this@HomeActivity)
|
val store = CredentialStore(this@HomeActivity)
|
||||||
val hidden = store.getHiddenMibProfileIds()
|
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||||
val allVisible = app.mibProfiles.filter { hidden.isEmpty() || it.profileId !in hidden }
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
// Try P47 for all visible profiles (fast path, works for multi-profile)
|
val allVisible = profiles.filter { hidden.isEmpty() || it.profileId !in hidden }
|
||||||
val sess = app.mibSession
|
val sess = app.mibSessions[loginId]
|
||||||
if (sess != null && allVisible.isNotEmpty()) {
|
if (sess != null && allVisible.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
val accounts = MibLoginFlow(store).fetchAllProfiles(sess, allVisible, src.loginTag)
|
val accounts = app.mibFlowFor(loginId).fetchAllProfiles(sess, allVisible, src.loginTag)
|
||||||
if (accounts.isNotEmpty()) return@withContext accounts
|
if (accounts.isNotEmpty()) return@withContext accounts
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
// P47 returned nothing — re-login to get fresh balances (handles single-profile A41 path)
|
val creds = store.loadMibCredentials(loginId) ?: return@withContext null
|
||||||
val creds = store.loadMibCredentials() ?: return@withContext null
|
|
||||||
try {
|
try {
|
||||||
val flow = MibLoginFlow(store)
|
val flow = MibLoginFlow(store)
|
||||||
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
app.mibSession = flow.lastSession
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
store.saveMibProfiles(flow.lastProfiles)
|
app.mibLoginFlows[loginId] = flow
|
||||||
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
accounts.takeIf { it.isNotEmpty() }
|
accounts.takeIf { it.isNotEmpty() }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
// Replace all MIB accounts with fresh data
|
// Replace accounts for this MIB login
|
||||||
val others = current.filter { it.bank != "MIB" }
|
val others = current.filter { it.loginTag != src.loginTag }
|
||||||
AccountCache.save(this@HomeActivity, fresh)
|
val newMibAccounts = app.mibAccounts.filter { it.loginTag != src.loginTag } + fresh
|
||||||
|
app.mibAccounts = newMibAccounts
|
||||||
|
AccountCache.save(this@HomeActivity, newMibAccounts)
|
||||||
viewModel.accounts.postValue(others + fresh)
|
viewModel.accounts.postValue(others + fresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
|
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = MibLoginFlow(CredentialStore(this))
|
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||||
val client = MibFinancingClient()
|
val client = MibFinancingClient()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ class OtpFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
|
||||||
val entries = mutableListOf<OtpEntry>()
|
val entries = mutableListOf<OtpEntry>()
|
||||||
store.loadMibCredentials()?.let { creds ->
|
for (loginId in store.getMibLoginIds()) {
|
||||||
val name = store.loadMibFullName()
|
val creds = store.loadMibCredentials(loginId) ?: continue
|
||||||
|
val name = store.loadMibFullName(loginId)
|
||||||
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
||||||
}
|
}
|
||||||
for (loginId in store.getBmlLoginIds()) {
|
for (loginId in store.getBmlLoginIds()) {
|
||||||
@@ -88,20 +89,23 @@ class OtpFragment : Fragment() {
|
|||||||
// Fetch real names in background if not yet cached, then refresh labels
|
// Fetch real names in background if not yet cached, then refresh labels
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
var changed = false
|
var changed = false
|
||||||
if (store.loadMibFullName() == null) {
|
for (loginId in store.getMibLoginIds()) {
|
||||||
app.mibSession?.let { session ->
|
if (store.loadMibFullName(loginId) == null) {
|
||||||
|
val session = app.mibSessions[loginId] ?: continue
|
||||||
|
val flow = app.mibFlowFor(loginId)
|
||||||
val profile = withContext(Dispatchers.IO) {
|
val profile = withContext(Dispatchers.IO) {
|
||||||
try { app.mibLoginFlow.fetchPersonalProfile(session) } catch (_: Exception) { null }
|
try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
store.saveMibUserProfile(CredentialStore.MibUserProfile(
|
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
username = profile.username,
|
username = profile.username,
|
||||||
email = profile.email,
|
email = profile.email,
|
||||||
mobile = profile.mobile,
|
mobile = profile.mobile,
|
||||||
enrolled = profile.enrolled
|
enrolled = profile.enrolled
|
||||||
))
|
))
|
||||||
val idx = entries.indexOfFirst { it.seed == store.loadMibCredentials()?.otpSeed }
|
val seed = store.loadMibCredentials(loginId)?.otpSeed
|
||||||
|
val idx = entries.indexOfFirst { it.seed == seed }
|
||||||
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.fullName}"); changed = true }
|
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.fullName}"); changed = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,18 +60,18 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
val container = binding.loginsContainer
|
val container = binding.loginsContainer
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
val hasMib = store.hasMibCredentials()
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
|
|
||||||
binding.tvLoginsTitle.visibility = if (hasMib || bmlLoginIds.isNotEmpty() || hasFahipay) View.VISIBLE else View.GONE
|
binding.tvLoginsTitle.visibility = if (mibLoginIds.isNotEmpty() || bmlLoginIds.isNotEmpty() || fahipayLoginIds.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (hasMib) {
|
for (loginId in mibLoginIds) {
|
||||||
val profile = store.loadMibUserProfile()
|
val profile = store.loadMibUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
||||||
val mibProfiles = store.loadMibProfiles()
|
val mibProfiles = store.loadMibProfiles(loginId)
|
||||||
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
||||||
showMibLoginDetails(store, profile, mibProfiles)
|
showMibLoginDetails(store, loginId, profile, mibProfiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFahipay) {
|
for (loginId in fahipayLoginIds) {
|
||||||
val profile = store.loadFahipayUserProfile()
|
val profile = store.loadFahipayUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
||||||
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
||||||
showLoginDetails(
|
showLoginDetails(
|
||||||
@@ -111,7 +111,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
||||||
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
|
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
|
||||||
}.trim(),
|
}.trim(),
|
||||||
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } }
|
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,12 +145,13 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
|
|
||||||
private fun showMibLoginDetails(
|
private fun showMibLoginDetails(
|
||||||
store: CredentialStore,
|
store: CredentialStore,
|
||||||
|
loginId: String,
|
||||||
profile: CredentialStore.MibUserProfile?,
|
profile: CredentialStore.MibUserProfile?,
|
||||||
mibProfiles: List<MibProfile>
|
mibProfiles: List<MibProfile>
|
||||||
) {
|
) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
val dp = ctx.resources.displayMetrics.density
|
val dp = ctx.resources.displayMetrics.density
|
||||||
val originalHidden = store.getHiddenMibProfileIds()
|
val originalHidden = store.getHiddenMibProfileIds(loginId)
|
||||||
val hidden = originalHidden.toMutableSet()
|
val hidden = originalHidden.toMutableSet()
|
||||||
|
|
||||||
val scroll = android.widget.ScrollView(ctx)
|
val scroll = android.widget.ScrollView(ctx)
|
||||||
@@ -231,7 +232,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
.setPositiveButton(R.string.save, null) // null — set manually after show()
|
.setPositiveButton(R.string.save, null) // null — set manually after show()
|
||||||
.setNeutralButton(R.string.close, null)
|
.setNeutralButton(R.string.close, null)
|
||||||
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
||||||
confirmLogout(getString(R.string.mib_name)) { logoutMib(store) }
|
confirmLogout(getString(R.string.mib_name)) { logoutMib(store, loginId) }
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.setOnClickListener {
|
saveBtn.setOnClickListener {
|
||||||
store.setHiddenMibProfileIds(hidden)
|
store.setHiddenMibProfileIds(loginId, hidden)
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
(activity as? HomeActivity)?.relogin()
|
(activity as? HomeActivity)?.relogin()
|
||||||
@@ -272,12 +273,16 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logoutMib(store: CredentialStore) {
|
private fun logoutMib(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
store.clearMibCredentials()
|
store.clearMibCredentials(loginId)
|
||||||
ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply()
|
ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.accounts = emptyList(); app.mibSession = null; app.mibProfiles = emptyList()
|
app.mibSessions.remove(loginId)
|
||||||
|
app.mibProfilesMap.remove(loginId)
|
||||||
|
app.mibLoginFlows.remove(loginId)
|
||||||
|
app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" }
|
||||||
|
app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" }
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
(activity as HomeActivity).relogin()
|
(activity as HomeActivity).relogin()
|
||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
@@ -294,11 +299,12 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logoutFahipay(store: CredentialStore) {
|
private fun logoutFahipay(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
store.clearFahipayCredentials(); store.clearFahipaySession()
|
store.clearFahipayCredentials(loginId)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.fahipaySession = null; app.fahipayAccounts = emptyList()
|
app.fahipaySessions.remove(loginId)
|
||||||
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != "fahipay_$loginId" }
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
(activity as HomeActivity).relogin()
|
(activity as HomeActivity).relogin()
|
||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ class TransferFragment : Fragment() {
|
|||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: MibAccount? = null
|
||||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
private val session get() = selectedAccount
|
||||||
|
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
||||||
|
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
||||||
private fun bmlSessionFor(account: MibAccount?) =
|
private fun bmlSessionFor(account: MibAccount?) =
|
||||||
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
||||||
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
||||||
@@ -244,7 +246,7 @@ class TransferFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = app.mibLoginFlow.fetchProfileImage(sess, hash) ?: return@launch
|
val base64 = app.anyMibFlow()?.fetchProfileImage(sess, hash) ?: return@launch
|
||||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -706,12 +708,14 @@ class TransferFragment : Fragment() {
|
|||||||
): Triple<Boolean, String, TransferReceiptData?> {
|
): Triple<Boolean, String, TransferReceiptData?> {
|
||||||
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
val loginId = src.loginTag.removePrefix("mib_")
|
||||||
// Switch to the profile that owns the source account
|
// Switch to the profile that owns the source account
|
||||||
if (src.profileId.isNotBlank()) {
|
if (src.profileId.isNotBlank()) {
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId }
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
||||||
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||||
}
|
}
|
||||||
val otp = CredentialStore(requireContext()).loadMibCredentials()?.otpSeed
|
val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed
|
||||||
?.let { Totp.generate(it) }
|
?.let { Totp.generate(it) }
|
||||||
?: return Triple(false, "OTP unavailable", null)
|
?: return Triple(false, "OTP unavailable", null)
|
||||||
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
||||||
@@ -878,7 +882,7 @@ class TransferFragment : Fragment() {
|
|||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (isProfile) {
|
val base64 = if (isProfile) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val mibSession = app.mibSession
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val newTransactions = withContext(Dispatchers.IO) {
|
val newTransactions = withContext(Dispatchers.IO) {
|
||||||
@@ -189,7 +188,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
// Fahipay accounts
|
// Fahipay accounts
|
||||||
val fahipayStates = activeStates.filter { it.account.bank == "FAHIPAY" }
|
val fahipayStates = activeStates.filter { it.account.bank == "FAHIPAY" }
|
||||||
for (state in fahipayStates) {
|
for (state in fahipayStates) {
|
||||||
val session = app.fahipaySession ?: continue
|
val session = app.fahipaySessionFor(state.account) ?: continue
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val flow = FahipayLoginFlow()
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
flow.setSessionCookie(session.sessionCookie)
|
||||||
@@ -207,24 +206,27 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
|
|
||||||
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
||||||
val mibStates = activeStates.filter { it.account.bank == "MIB" }
|
val mibStates = activeStates.filter { it.account.bank == "MIB" }
|
||||||
for ((profileId, states) in mibStates.groupBy { it.account.profileId }) {
|
for ((loginId, loginStates) in mibStates.groupBy { it.account.loginTag.removePrefix("mib_") }) {
|
||||||
val session = mibSession ?: break
|
val session = app.mibSessions[loginId] ?: continue
|
||||||
app.mibMutex.withLock {
|
for ((profileId, states) in loginStates.groupBy { it.account.profileId }) {
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == profileId }
|
app.mibMutex.withLock {
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(session, profile)
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
for (state in states) {
|
val profile = profiles.firstOrNull { it.profileId == profileId }
|
||||||
try {
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile)
|
||||||
val (list, total) = MibHistoryClient().fetchHistory(
|
for (state in states) {
|
||||||
session = session,
|
try {
|
||||||
accountNo = state.account.accountNumber,
|
val (list, total) = MibHistoryClient().fetchHistory(
|
||||||
accountDisplayName = state.account.accountBriefName,
|
session = session,
|
||||||
start = state.mibNextStart,
|
accountNo = state.account.accountNumber,
|
||||||
pageSize = pageSize
|
accountDisplayName = state.account.accountBriefName,
|
||||||
)
|
start = state.mibNextStart,
|
||||||
if (total > 0) state.mibTotalCount = total
|
pageSize = pageSize
|
||||||
state.mibNextStart += list.size.coerceAtLeast(pageSize)
|
)
|
||||||
results.addAll(list)
|
if (total > 0) state.mibTotalCount = total
|
||||||
} catch (_: Exception) {}
|
state.mibNextStart += list.size.coerceAtLeast(pageSize)
|
||||||
|
results.addAll(list)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +280,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||||
|
|||||||
@@ -154,11 +154,11 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
|
|
||||||
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (isProfile) {
|
val base64 = if (isProfile) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.btnLogin.isEnabled = false
|
binding.btnLogin.isEnabled = false
|
||||||
|
|
||||||
val passwordHash = MibLoginFlow.hashPassword(password)
|
val passwordHash = MibLoginFlow.hashPassword(password)
|
||||||
|
val loginId = username
|
||||||
val flow = MibLoginFlow(CredentialStore(requireContext()))
|
val flow = MibLoginFlow(CredentialStore(requireContext()))
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
@@ -178,11 +179,12 @@ class CredentialsFragment : Fragment() {
|
|||||||
flow.login(username, passwordHash, otpSeed)
|
flow.login(username, passwordHash, otpSeed)
|
||||||
}
|
}
|
||||||
val store = CredentialStore(requireContext())
|
val store = CredentialStore(requireContext())
|
||||||
store.saveMibCredentials(username, passwordHash, otpSeed)
|
store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
flow.lastSession?.let { s ->
|
flow.lastSession?.let { s ->
|
||||||
val profile = flow.fetchPersonalProfile(s)
|
val profile = flow.fetchPersonalProfile(s)
|
||||||
if (profile != null) store.saveMibUserProfile(
|
if (profile != null) store.saveMibUserProfile(
|
||||||
|
loginId,
|
||||||
CredentialStore.MibUserProfile(
|
CredentialStore.MibUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
username = profile.username,
|
username = profile.username,
|
||||||
@@ -193,12 +195,15 @@ class CredentialsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AccountCache.save(requireContext(), accounts)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
CredentialStore(requireContext()).saveMibProfiles(flow.lastProfiles)
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.accounts = accounts
|
// Merge with any existing MIB accounts from other logins
|
||||||
app.mibSession = flow.lastSession
|
app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" } + accounts
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" } + accounts
|
||||||
|
AccountCache.save(requireContext(), app.mibAccounts)
|
||||||
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
|
app.mibLoginFlows[loginId] = flow
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@@ -375,11 +380,13 @@ class CredentialsFragment : Fragment() {
|
|||||||
val b = flow.fetchBalance(session)
|
val b = flow.fetchBalance(session)
|
||||||
Pair(p, b)
|
Pair(p, b)
|
||||||
}
|
}
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
val loginId = profile.profileId
|
||||||
|
val loginTag = "fahipay_$loginId"
|
||||||
val account = flow.buildAccount(profile, balance, loginTag)
|
val account = flow.buildAccount(profile, balance, loginTag)
|
||||||
store.saveFahipayCredentials(idCard, password)
|
store.saveFahipayCredentials(loginId, idCard, password)
|
||||||
store.saveFahipaySession(session.authId, session.sessionCookie)
|
store.saveFahipaySession(loginId, session.authId, session.sessionCookie)
|
||||||
store.saveFahipayUserProfile(
|
store.saveFahipayUserProfile(
|
||||||
|
loginId,
|
||||||
CredentialStore.FahipayUserProfile(
|
CredentialStore.FahipayUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
email = profile.email,
|
email = profile.email,
|
||||||
@@ -390,11 +397,11 @@ class CredentialsFragment : Fragment() {
|
|||||||
linkedAccounts = profile.linkedAccounts
|
linkedAccounts = profile.linkedAccounts
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AccountCache.saveFahipay(requireContext(), listOf(account))
|
AccountCache.saveFahipay(requireContext(), loginId, listOf(account))
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = listOf(account)
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != loginTag } + listOf(account)
|
||||||
app.accounts = app.accounts + listOf(account)
|
app.accounts = app.accounts.filter { it.loginTag != loginTag } + listOf(account)
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ object AccountCache {
|
|||||||
|
|
||||||
private const val PREFS = "account_cache"
|
private const val PREFS = "account_cache"
|
||||||
private const val KEY_MIB = "mib_accounts"
|
private const val KEY_MIB = "mib_accounts"
|
||||||
private const val KEY_FAHIPAY = "fahipay_accounts"
|
|
||||||
|
|
||||||
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
||||||
|
private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId"
|
||||||
|
|
||||||
fun save(context: Context, accounts: List<MibAccount>) {
|
fun save(context: Context, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
@@ -93,7 +92,7 @@ object AccountCache {
|
|||||||
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||||
loginIds.flatMap { loadBml(context, it) }
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
|
fun saveFahipay(context: Context, loginId: String, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -113,12 +112,12 @@ object AccountCache {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.edit().putString(KEY_FAHIPAY, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipay(context: Context): List<MibAccount> {
|
fun loadFahipay(context: Context, loginId: String): List<MibAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_FAHIPAY, null) ?: return emptyList()
|
.getString(fahipayKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
@@ -144,6 +143,9 @@ object AccountCache {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadFahipay(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||||
|
loginIds.flatMap { loadFahipay(context, it) }
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ object ContactManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
||||||
val sess = app.mibSession ?: return false
|
val sess = app.anyMibSession() ?: return false
|
||||||
return try {
|
return try {
|
||||||
if (contact.profileId.isNotBlank()) {
|
if (contact.profileId.isNotBlank()) {
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == contact.profileId }
|
val (loginId, profile) = app.mibProfilesMap.entries
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
.firstNotNullOfOrNull { (id, profiles) ->
|
||||||
|
profiles.firstOrNull { it.profileId == contact.profileId }?.let { id to it }
|
||||||
|
} ?: (null to null)
|
||||||
|
if (profile != null && loginId != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||||
}
|
}
|
||||||
MibContactsClient().deleteContact(sess, contact.id)
|
MibContactsClient().deleteContact(sess, contact.id)
|
||||||
} catch (_: Exception) { false }
|
} catch (_: Exception) { false }
|
||||||
|
|||||||
@@ -21,75 +21,131 @@ class CredentialStore(context: Context) {
|
|||||||
data class BmlCredentials(val username: String, val password: String, val otpSeed: String)
|
data class BmlCredentials(val username: String, val password: String, val otpSeed: String)
|
||||||
data class FahipayCredentials(val idCard: String, val password: String)
|
data class FahipayCredentials(val idCard: String, val password: String)
|
||||||
|
|
||||||
// ── MIB login credentials ─────────────────────────────────────────────────
|
// ── MIB login credentials (multi-login, keyed by loginId = username) ─────
|
||||||
|
|
||||||
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
fun getMibLoginIds(): List<String> {
|
||||||
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
|
maybeMigrateLegacyMib()
|
||||||
|
val json = prefs.getString("mib_login_ids", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(json)
|
||||||
|
(0 until arr.length()).map { arr.getString(it) }
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
fun hasMibCredentials(): Boolean = getMibLoginIds().isNotEmpty()
|
||||||
|
|
||||||
|
private fun addMibLoginId(loginId: String) {
|
||||||
|
val ids = getMibLoginIds().toMutableList()
|
||||||
|
if (loginId !in ids) {
|
||||||
|
ids.add(loginId)
|
||||||
|
prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeMibLoginId(loginId: String) {
|
||||||
|
val ids = getMibLoginIds().toMutableList()
|
||||||
|
if (ids.remove(loginId))
|
||||||
|
prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMibCredentials(loginId: String, username: String, passwordHash: String, otpSeed: String) {
|
||||||
|
addMibLoginId(loginId)
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("mib_enc_username", encrypt(username, key))
|
.putString("mib_${loginId}_enc_password_hash", encrypt(passwordHash, key))
|
||||||
.putString("mib_enc_password_hash", encrypt(passwordHash, key))
|
.putString("mib_${loginId}_enc_otp_seed", encrypt(otpSeed, key))
|
||||||
.putString("mib_enc_otp_seed", encrypt(otpSeed, key))
|
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibCredentials(): MibCredentials? {
|
fun loadMibCredentials(loginId: String): MibCredentials? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encUsername = prefs.getString("mib_enc_username", null) ?: return null
|
val encHash = prefs.getString("mib_${loginId}_enc_password_hash", null) ?: return null
|
||||||
val encHash = prefs.getString("mib_enc_password_hash", null) ?: return null
|
val encSeed = prefs.getString("mib_${loginId}_enc_otp_seed", null) ?: return null
|
||||||
val encSeed = prefs.getString("mib_enc_otp_seed", null) ?: return null
|
|
||||||
return try {
|
return try {
|
||||||
MibCredentials(
|
MibCredentials(loginId, decrypt(encHash, key), decrypt(encSeed, key))
|
||||||
decrypt(encUsername, key),
|
|
||||||
decrypt(encHash, key),
|
|
||||||
decrypt(encSeed, key)
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearMibCredentials() {
|
fun clearMibCredentials(loginId: String) {
|
||||||
|
removeMibLoginId(loginId)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("mib_enc_username")
|
.remove("mib_${loginId}_enc_password_hash")
|
||||||
.remove("mib_enc_password_hash")
|
.remove("mib_${loginId}_enc_otp_seed")
|
||||||
.remove("mib_enc_otp_seed")
|
.remove("mib_${loginId}_enc_key1")
|
||||||
.remove("mib_enc_key1")
|
.remove("mib_${loginId}_enc_key2")
|
||||||
.remove("mib_enc_key2")
|
.remove("mib_${loginId}_enc_app_id")
|
||||||
.remove("mib_enc_app_id")
|
.remove("mib_${loginId}_all_profiles")
|
||||||
|
.remove("mib_${loginId}_enc_profile")
|
||||||
|
.remove("mib_${loginId}_enc_full_name")
|
||||||
|
.remove("mib_${loginId}_hidden_profile_ids")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MIB session keys (key1/key2) and app ID ───────────────────────────────
|
// ── MIB session keys (key1/key2) and app ID (per loginId) ────────────────
|
||||||
|
|
||||||
fun saveMibKeys(key1: String, key2: String) {
|
fun saveMibKeys(loginId: String, key1: String, key2: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("mib_enc_key1", encrypt(key1, key))
|
.putString("mib_${loginId}_enc_key1", encrypt(key1, key))
|
||||||
.putString("mib_enc_key2", encrypt(key2, key))
|
.putString("mib_${loginId}_enc_key2", encrypt(key2, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibKeys(): Pair<String, String>? {
|
fun loadMibKeys(loginId: String): Pair<String, String>? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encKey1 = prefs.getString("mib_enc_key1", null) ?: return null
|
val encKey1 = prefs.getString("mib_${loginId}_enc_key1", null) ?: return null
|
||||||
val encKey2 = prefs.getString("mib_enc_key2", null) ?: return null
|
val encKey2 = prefs.getString("mib_${loginId}_enc_key2", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
Pair(decrypt(encKey1, key), decrypt(encKey2, key))
|
Pair(decrypt(encKey1, key), decrypt(encKey2, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMibAppId(id: String) {
|
fun saveMibAppId(loginId: String, id: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("mib_enc_app_id", encrypt(id, key)).apply()
|
prefs.edit().putString("mib_${loginId}_enc_app_id", encrypt(id, key)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibAppId(): String? {
|
fun loadMibAppId(loginId: String): String? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_app_id", null) ?: return null
|
val enc = prefs.getString("mib_${loginId}_enc_app_id", null) ?: return null
|
||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One-time migration: if old single-login MIB data exists, move it to per-loginId storage. */
|
||||||
|
private var migrationChecked = false
|
||||||
|
private fun maybeMigrateLegacyMib() {
|
||||||
|
if (migrationChecked) return
|
||||||
|
migrationChecked = true
|
||||||
|
if (prefs.contains("mib_login_ids")) return // already migrated
|
||||||
|
val encUsername = prefs.getString("mib_enc_username", null) ?: return
|
||||||
|
val key = try { getOrCreateKey() } catch (_: Exception) { return }
|
||||||
|
val loginId = try { decrypt(encUsername, key) } catch (_: Exception) { return }
|
||||||
|
val editor = prefs.edit()
|
||||||
|
// Migrate credentials
|
||||||
|
prefs.getString("mib_enc_password_hash", null)?.let { editor.putString("mib_${loginId}_enc_password_hash", it) }
|
||||||
|
prefs.getString("mib_enc_otp_seed", null)?.let { editor.putString("mib_${loginId}_enc_otp_seed", it) }
|
||||||
|
prefs.getString("mib_enc_key1", null)?.let { editor.putString("mib_${loginId}_enc_key1", it) }
|
||||||
|
prefs.getString("mib_enc_key2", null)?.let { editor.putString("mib_${loginId}_enc_key2", it) }
|
||||||
|
prefs.getString("mib_enc_app_id", null)?.let { editor.putString("mib_${loginId}_enc_app_id", it) }
|
||||||
|
prefs.getString("mib_all_profiles", null)?.let { editor.putString("mib_${loginId}_all_profiles", it) }
|
||||||
|
prefs.getString("mib_enc_profile", null)?.let { editor.putString("mib_${loginId}_enc_profile", it) }
|
||||||
|
prefs.getString("mib_enc_full_name", null)?.let { editor.putString("mib_${loginId}_enc_full_name", it) }
|
||||||
|
prefs.getStringSet("mib_hidden_profile_ids", null)?.let { editor.putStringSet("mib_${loginId}_hidden_profile_ids", it) }
|
||||||
|
// Register the login ID and clear legacy keys
|
||||||
|
editor.putString("mib_login_ids", org.json.JSONArray(listOf(loginId)).toString())
|
||||||
|
editor.remove("mib_enc_username")
|
||||||
|
editor.remove("mib_enc_password_hash")
|
||||||
|
editor.remove("mib_enc_otp_seed")
|
||||||
|
editor.remove("mib_enc_key1")
|
||||||
|
editor.remove("mib_enc_key2")
|
||||||
|
editor.remove("mib_enc_app_id")
|
||||||
|
editor.remove("mib_all_profiles")
|
||||||
|
editor.remove("mib_enc_profile")
|
||||||
|
editor.remove("mib_enc_full_name")
|
||||||
|
editor.remove("mib_hidden_profile_ids")
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
||||||
|
|
||||||
fun getBmlLoginIds(): List<String> {
|
fun getBmlLoginIds(): List<String> {
|
||||||
@@ -171,58 +227,81 @@ class CredentialStore(context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay login credentials ─────────────────────────────────────────────
|
// ── Fahipay login credentials (multi-login, keyed by loginId = profileId) ──
|
||||||
|
|
||||||
fun saveFahipayCredentials(idCard: String, password: String) {
|
fun getFahipayLoginIds(): List<String> {
|
||||||
|
maybeMigrateLegacyFahipay()
|
||||||
|
val json = prefs.getString("fahipay_login_ids", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(json)
|
||||||
|
(0 until arr.length()).map { arr.getString(it) }
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasFahipayCredentials(): Boolean = getFahipayLoginIds().isNotEmpty()
|
||||||
|
|
||||||
|
private fun addFahipayLoginId(loginId: String) {
|
||||||
|
val ids = getFahipayLoginIds().toMutableList()
|
||||||
|
if (loginId !in ids) {
|
||||||
|
ids.add(loginId)
|
||||||
|
prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFahipayLoginId(loginId: String) {
|
||||||
|
val ids = getFahipayLoginIds().toMutableList()
|
||||||
|
if (ids.remove(loginId))
|
||||||
|
prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFahipayCredentials(loginId: String, idCard: String, password: String) {
|
||||||
|
addFahipayLoginId(loginId)
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("fahipay_enc_id_card", encrypt(idCard, key))
|
.putString("fahipay_${loginId}_enc_id_card", encrypt(idCard, key))
|
||||||
.putString("fahipay_enc_password", encrypt(password, key))
|
.putString("fahipay_${loginId}_enc_password", encrypt(password, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipayCredentials(): FahipayCredentials? {
|
fun loadFahipayCredentials(loginId: String): FahipayCredentials? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encId = prefs.getString("fahipay_enc_id_card", null) ?: return null
|
val encId = prefs.getString("fahipay_${loginId}_enc_id_card", null) ?: return null
|
||||||
val encPw = prefs.getString("fahipay_enc_password", null) ?: return null
|
val encPw = prefs.getString("fahipay_${loginId}_enc_password", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
FahipayCredentials(decrypt(encId, key), decrypt(encPw, key))
|
FahipayCredentials(decrypt(encId, key), decrypt(encPw, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFahipayCredentials() {
|
fun clearFahipayCredentials(loginId: String) {
|
||||||
|
removeFahipayLoginId(loginId)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("fahipay_enc_id_card")
|
.remove("fahipay_${loginId}_enc_id_card")
|
||||||
.remove("fahipay_enc_password")
|
.remove("fahipay_${loginId}_enc_password")
|
||||||
|
.remove("fahipay_${loginId}_enc_auth_id")
|
||||||
|
.remove("fahipay_${loginId}_enc_session_cookie")
|
||||||
|
.remove("fahipay_${loginId}_enc_profile")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay session (authId + __Secure-sess cookie) ───────────────────────
|
// ── Fahipay session (authId + __Secure-sess cookie) (per loginId) ─────────
|
||||||
|
|
||||||
fun saveFahipaySession(authId: String, sessionCookie: String) {
|
fun saveFahipaySession(loginId: String, authId: String, sessionCookie: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("fahipay_enc_auth_id", encrypt(authId, key))
|
.putString("fahipay_${loginId}_enc_auth_id", encrypt(authId, key))
|
||||||
.putString("fahipay_enc_session_cookie", encrypt(sessionCookie, key))
|
.putString("fahipay_${loginId}_enc_session_cookie", encrypt(sessionCookie, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipaySession(): Pair<String, String>? {
|
fun loadFahipaySession(loginId: String): Pair<String, String>? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encAuth = prefs.getString("fahipay_enc_auth_id", null) ?: return null
|
val encAuth = prefs.getString("fahipay_${loginId}_enc_auth_id", null) ?: return null
|
||||||
val encCookie = prefs.getString("fahipay_enc_session_cookie", null) ?: return null
|
val encCookie = prefs.getString("fahipay_${loginId}_enc_session_cookie", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
Pair(decrypt(encAuth, key), decrypt(encCookie, key))
|
Pair(decrypt(encAuth, key), decrypt(encCookie, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFahipaySession() {
|
|
||||||
prefs.edit()
|
|
||||||
.remove("fahipay_enc_auth_id")
|
|
||||||
.remove("fahipay_enc_session_cookie")
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Fahipay device UUID (generated once, shared across all Fahipay accounts) ─
|
// ── Fahipay device UUID (generated once, shared across all Fahipay accounts) ─
|
||||||
|
|
||||||
fun getOrCreateFahipayDeviceUuid(): String {
|
fun getOrCreateFahipayDeviceUuid(): String {
|
||||||
@@ -236,7 +315,7 @@ class CredentialStore(context: Context) {
|
|||||||
return uuid
|
return uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay user profile ──────────────────────────────────────────────────
|
// ── Fahipay user profile (per loginId) ────────────────────────────────────
|
||||||
|
|
||||||
data class FahipayUserProfile(
|
data class FahipayUserProfile(
|
||||||
val fullName: String,
|
val fullName: String,
|
||||||
@@ -248,7 +327,7 @@ class CredentialStore(context: Context) {
|
|||||||
val linkedAccounts: String
|
val linkedAccounts: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun saveFahipayUserProfile(p: FahipayUserProfile) {
|
fun saveFahipayUserProfile(loginId: String, p: FahipayUserProfile) {
|
||||||
val json = org.json.JSONObject().apply {
|
val json = org.json.JSONObject().apply {
|
||||||
put("fullName", p.fullName)
|
put("fullName", p.fullName)
|
||||||
put("email", p.email)
|
put("email", p.email)
|
||||||
@@ -259,12 +338,12 @@ class CredentialStore(context: Context) {
|
|||||||
put("linkedAccounts", p.linkedAccounts)
|
put("linkedAccounts", p.linkedAccounts)
|
||||||
}.toString()
|
}.toString()
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("fahipay_enc_profile", encrypt(json, key)).apply()
|
prefs.edit().putString("fahipay_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipayUserProfile(): FahipayUserProfile? {
|
fun loadFahipayUserProfile(loginId: String): FahipayUserProfile? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("fahipay_enc_profile", null) ?: return null
|
val enc = prefs.getString("fahipay_${loginId}_enc_profile", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val o = org.json.JSONObject(decrypt(enc, key))
|
val o = org.json.JSONObject(decrypt(enc, key))
|
||||||
FahipayUserProfile(
|
FahipayUserProfile(
|
||||||
@@ -279,6 +358,33 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One-time migration: if old single-login Fahipay data exists, move it to per-loginId storage. */
|
||||||
|
private var fahipayMigrationChecked = false
|
||||||
|
private fun maybeMigrateLegacyFahipay() {
|
||||||
|
if (fahipayMigrationChecked) return
|
||||||
|
fahipayMigrationChecked = true
|
||||||
|
if (prefs.contains("fahipay_login_ids")) return // already migrated
|
||||||
|
val encProfile = prefs.getString("fahipay_enc_profile", null) ?: return
|
||||||
|
val key = try { getOrCreateKey() } catch (_: Exception) { return }
|
||||||
|
val loginId = try {
|
||||||
|
val o = org.json.JSONObject(decrypt(encProfile, key))
|
||||||
|
o.optString("profileId").takeIf { it.isNotBlank() }
|
||||||
|
} catch (_: Exception) { null } ?: return
|
||||||
|
val editor = prefs.edit()
|
||||||
|
prefs.getString("fahipay_enc_id_card", null)?.let { editor.putString("fahipay_${loginId}_enc_id_card", it) }
|
||||||
|
prefs.getString("fahipay_enc_password", null)?.let { editor.putString("fahipay_${loginId}_enc_password", it) }
|
||||||
|
prefs.getString("fahipay_enc_auth_id", null)?.let { editor.putString("fahipay_${loginId}_enc_auth_id", it) }
|
||||||
|
prefs.getString("fahipay_enc_session_cookie", null)?.let { editor.putString("fahipay_${loginId}_enc_session_cookie", it) }
|
||||||
|
editor.putString("fahipay_${loginId}_enc_profile", encProfile)
|
||||||
|
editor.putString("fahipay_login_ids", org.json.JSONArray(listOf(loginId)).toString())
|
||||||
|
editor.remove("fahipay_enc_id_card")
|
||||||
|
editor.remove("fahipay_enc_password")
|
||||||
|
editor.remove("fahipay_enc_auth_id")
|
||||||
|
editor.remove("fahipay_enc_session_cookie")
|
||||||
|
editor.remove("fahipay_enc_profile")
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Security credential (PIN / pattern hash) ──────────────────────────────
|
// ── Security credential (PIN / pattern hash) ──────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,9 +435,9 @@ class CredentialStore(context: Context) {
|
|||||||
val birthdate: String
|
val birthdate: String
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── MIB operating profiles (all profiles, regardless of visibility) ───────
|
// ── MIB operating profiles (per loginId) ─────────────────────────────────
|
||||||
|
|
||||||
fun saveMibProfiles(profiles: List<sh.sar.basedbank.api.mib.MibProfile>) {
|
fun saveMibProfiles(loginId: String, profiles: List<sh.sar.basedbank.api.mib.MibProfile>) {
|
||||||
val arr = org.json.JSONArray()
|
val arr = org.json.JSONArray()
|
||||||
for (p in profiles) {
|
for (p in profiles) {
|
||||||
arr.put(org.json.JSONObject().apply {
|
arr.put(org.json.JSONObject().apply {
|
||||||
@@ -342,11 +448,11 @@ class CredentialStore(context: Context) {
|
|||||||
put("color", p.color)
|
put("color", p.color)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
prefs.edit().putString("mib_all_profiles", arr.toString()).apply()
|
prefs.edit().putString("mib_${loginId}_all_profiles", arr.toString()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibProfiles(): List<sh.sar.basedbank.api.mib.MibProfile> {
|
fun loadMibProfiles(loginId: String): List<sh.sar.basedbank.api.mib.MibProfile> {
|
||||||
val raw = prefs.getString("mib_all_profiles", null) ?: return emptyList()
|
val raw = prefs.getString("mib_${loginId}_all_profiles", null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = org.json.JSONArray(raw)
|
val arr = org.json.JSONArray(raw)
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
@@ -366,19 +472,18 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMibFullName(name: String) {
|
fun saveMibFullName(loginId: String, name: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("mib_enc_full_name", encrypt(name, key)).apply()
|
prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(name, key)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibFullName(): String? {
|
fun loadMibFullName(loginId: String): String? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_full_name", null) ?: return null
|
val enc = prefs.getString("mib_${loginId}_enc_full_name", null) ?: return null
|
||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveMibUserProfile(loginId: String, p: MibUserProfile) {
|
||||||
fun saveMibUserProfile(p: MibUserProfile) {
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("fullName", p.fullName)
|
put("fullName", p.fullName)
|
||||||
put("username", p.username)
|
put("username", p.username)
|
||||||
@@ -387,14 +492,13 @@ class CredentialStore(context: Context) {
|
|||||||
put("enrolled", p.enrolled)
|
put("enrolled", p.enrolled)
|
||||||
}.toString()
|
}.toString()
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("mib_enc_profile", encrypt(json, key)).apply()
|
prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||||
// Keep the name in sync with the fast-path field
|
prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(p.fullName, key)).apply()
|
||||||
prefs.edit().putString("mib_enc_full_name", encrypt(p.fullName, key)).apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibUserProfile(): MibUserProfile? {
|
fun loadMibUserProfile(loginId: String): MibUserProfile? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_profile", null) ?: return null
|
val enc = prefs.getString("mib_${loginId}_enc_profile", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val o = JSONObject(decrypt(enc, key))
|
val o = JSONObject(decrypt(enc, key))
|
||||||
MibUserProfile(
|
MibUserProfile(
|
||||||
@@ -436,14 +540,14 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MIB profile visibility ────────────────────────────────────────────────
|
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
||||||
|
|
||||||
/** Returns the set of MIB profile IDs the user has chosen to hide from the app. */
|
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
|
||||||
fun getHiddenMibProfileIds(): Set<String> =
|
fun getHiddenMibProfileIds(loginId: String): Set<String> =
|
||||||
prefs.getStringSet("mib_hidden_profile_ids", emptySet()) ?: emptySet()
|
prefs.getStringSet("mib_${loginId}_hidden_profile_ids", emptySet()) ?: emptySet()
|
||||||
|
|
||||||
fun setHiddenMibProfileIds(ids: Set<String>) =
|
fun setHiddenMibProfileIds(loginId: String, ids: Set<String>) =
|
||||||
prefs.edit().putStringSet("mib_hidden_profile_ids", ids).apply()
|
prefs.edit().putStringSet("mib_${loginId}_hidden_profile_ids", ids).apply()
|
||||||
|
|
||||||
// ── Crypto primitives ─────────────────────────────────────────────────────
|
// ── Crypto primitives ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchFahipay(app: BasedBankApp): List<Transaction> {
|
private fun fetchFahipay(app: BasedBankApp): List<Transaction> {
|
||||||
val session = app.fahipaySession ?: return emptyList()
|
val session = app.fahipaySessionFor(account) ?: return emptyList()
|
||||||
val flow = FahipayLoginFlow()
|
val flow = FahipayLoginFlow()
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
flow.setSessionCookie(session.sessionCookie)
|
||||||
val (list, total) = flow.fetchHistory(
|
val (list, total) = flow.fetchHistory(
|
||||||
@@ -69,9 +69,11 @@ class HistoryFetcher(private val account: MibAccount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchMib(app: BasedBankApp, pageSize: Int): List<Transaction> {
|
private fun fetchMib(app: BasedBankApp, pageSize: Int): List<Transaction> {
|
||||||
val session = app.mibSession ?: return emptyList()
|
val loginId = account.loginTag.removePrefix("mib_")
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId }
|
val session = app.mibSessions[loginId] ?: return emptyList()
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(session, profile)
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
val profile = profiles.firstOrNull { it.profileId == account.profileId }
|
||||||
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile)
|
||||||
val (list, total) = MibHistoryClient().fetchHistory(
|
val (list, total) = MibHistoryClient().fetchHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountNo = account.accountNumber,
|
accountNo = account.accountNumber,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<string name="nav_dashboard">Dashboard</string>
|
<string name="nav_dashboard">Dashboard</string>
|
||||||
<string name="nav_add_account">Add Account</string>
|
<string name="nav_add_account">Add Login</string>
|
||||||
<string name="nav_accounts">Accounts</string>
|
<string name="nav_accounts">Accounts</string>
|
||||||
<string name="nav_contacts">Contacts</string>
|
<string name="nav_contacts">Contacts</string>
|
||||||
<string name="nav_activities">Activities</string>
|
<string name="nav_activities">Activities</string>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user