diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt index 1a3e49a..6c15d01 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt @@ -121,15 +121,27 @@ class BmlLoginFlow { twoFaResp.close() if (twoFaResp.code != 302) throw Exception("OTP verification failed — check your OTP seed") - // Step 5: GET /web/profile — returns list of profiles for this account + // Step 5: GET /web/profile — multi-profile accounts return a 200 with a profile picker; + // single-profile accounts skip the picker and redirect straight to /web/redirect with + // blaze_identity already set in the response cookies. val profileResp = client.newCall( Request.Builder().url("$BASE_URL/web/profile") .header("User-Agent", WEB_USER_AGENT).build() ).execute() + val profileCode = profileResp.code + val profileLocation = profileResp.header("Location") ?: "" val profileBody = profileResp.body?.string() ?: "" profileResp.close() - lastProfiles = parseProfiles(profileBody) + lastProfiles = if (profileCode == 302) { + // Any 302 from GET /web/profile means the server auto-activated the sole profile + // and blaze_identity is already set — no profile picker shown. + // Use username as a stable temporary profileId (unique per login); it will be + // replaced by the real BML customer ID after fetchUserInfo in finishBmlLogin(). + listOf(BmlProfile(profileId = username, name = "Personal", type = "Profile", profileType = "default", autoActivated = true)) + } else { + parseProfiles(profileBody) + } return lastProfiles } @@ -144,6 +156,13 @@ class BmlLoginFlow { * [requestBusinessOtp] + [submitBusinessOtp]. */ fun activateProfile(profile: BmlProfile, loginTag: String): BmlActivationResult { + // Single-profile accounts: server already activated during login() and set blaze_identity. + // autoActivated=true is the sentinel for this case — skip the profile GET entirely. + if (profile.autoActivated) { + val (session, accounts) = doOAuthAndFetchAccounts(loginTag, profile.name, profile.profileId) + return BmlActivationResult.Success(session, accounts) + } + val xsrf = xsrfToken() val reqBuilder = Request.Builder() .url("$BASE_URL/web/profile/${profile.profileId}") @@ -156,8 +175,9 @@ class BmlLoginFlow { resp.close() return when { - code == 409 || (code == 302 && "/web/redirect" in location) -> { - // Profile activated — blaze_identity cookie set in response headers + code == 409 || (code == 302 && "/web/profile/2fa/business" !in location) -> { + // Profile activated — blaze_identity cookie set in response headers. + // Any 302 that isn't to the business 2FA page means success. val (session, accounts) = doOAuthAndFetchAccounts(loginTag, profile.name, profile.profileId) BmlActivationResult.Success(session, accounts) } diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt index a8b9a7c..d54f75d 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlModels.kt @@ -11,7 +11,8 @@ data class BmlProfile( val profileId: String, val name: String, val type: String, // "Profile" (personal) or "Business" - val profileType: String // "default" or "business" + val profileType: String, // "default" or "business" + val autoActivated: Boolean = false // true for single-profile accounts where server skips the picker ) data class BmlOtpChannel( diff --git a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt index d1279bd..20273c3 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt @@ -429,17 +429,46 @@ class CredentialsFragment : Fragment() { if (anySession != null) { withContext(Dispatchers.IO) { val info = BmlLoginFlow().fetchUserInfo(anySession) - if (info != null) store.saveBmlUserProfile( - bmlLoginId, - CredentialStore.BmlUserProfile( - fullName = info.fullName, - email = info.email, - mobile = info.mobile, - customerId = info.customerId, - idCard = info.idCard, - birthdate = info.birthdate + if (info != null) { + store.saveBmlUserProfile( + bmlLoginId, + CredentialStore.BmlUserProfile( + fullName = info.fullName, + email = info.email, + mobile = info.mobile, + customerId = info.customerId, + idCard = info.idCard, + birthdate = info.birthdate + ) ) - ) + // Single-profile accounts used username as a temporary profileId. + // Replace it with the real BML customer ID so multi-login doesn't collide. + val customerId = info.customerId + if (customerId.isNotBlank()) { + val profiles = store.loadBmlProfiles(bmlLoginId) + val autoProfile = profiles.firstOrNull { it.autoActivated } + if (autoProfile != null && autoProfile.profileId != customerId) { + val oldId = autoProfile.profileId + // Re-key session in memory and storage + val session = app.bmlSessions.remove(oldId) + if (session != null) { + app.bmlSessions[customerId] = session + store.clearBmlProfileSession(oldId) + store.saveBmlProfileSession(customerId, session.accessToken, session.deviceId) + } + // Update stored profile list with the real ID + val updatedProfiles = profiles.map { + if (it.autoActivated) it.copy(profileId = customerId) else it + } + store.saveBmlProfiles(bmlLoginId, updatedProfiles) + app.bmlProfilesMap[bmlLoginId] = updatedProfiles + // Update accounts to use real profileId + bmlAccumulatedAccounts.replaceAll { acc -> + if (acc.profileId == oldId) acc.copy(profileId = customerId) else acc + } + } + } + } } } diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index 397f0fb..efd0362 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -217,6 +217,7 @@ class CredentialStore(context: Context) { put("name", p.name) put("type", p.type) put("profileType", p.profileType) + put("autoActivated", p.autoActivated) }) prefs.edit().putString("bml_${loginId}_profiles", arr.toString()).apply() } @@ -228,10 +229,11 @@ class CredentialStore(context: Context) { (0 until arr.length()).map { i -> val o = arr.getJSONObject(i) BmlProfile( - profileId = o.optString("profileId"), - name = o.optString("name"), - type = o.optString("type"), - profileType = o.optString("profileType", "default") + profileId = o.optString("profileId"), + name = o.optString("name"), + type = o.optString("type"), + profileType = o.optString("profileType", "default"), + autoActivated = o.optBoolean("autoActivated", false) ) } } catch (_: Exception) { emptyList() }