Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b116e88
Improve application password storing error logs for debugging
adalpari Mar 16, 2026
79cabe6
Avoid logging full response object to prevent potential data leak
adalpari Mar 16, 2026
0fdd1a2
Extract log helper to fix detekt LongMethod violation
adalpari Mar 16, 2026
bd3937e
Fix test expecting single siteStore.sites call
adalpari Mar 17, 2026
83c2831
Add Tracks and Sentry events for application password storing failures
adalpari Mar 17, 2026
b6934d1
Revert "Add Tracks and Sentry events for application password storing…
adalpari Mar 17, 2026
2b003be
Avoid logging user site URLs to prevent PII exposure
adalpari Mar 17, 2026
9d44e19
Add diagnostic error handling for application password login failures
jkmassel Mar 17, 2026
5246138
Send Sentry report on application password login failure
jkmassel Mar 17, 2026
1b647c4
Add analytics tracking and crash logging for app password storing fai…
adalpari Mar 17, 2026
dd53d61
Address code review: hide internal errors from toast, fix non-null as…
adalpari Mar 18, 2026
fc25020
Refactor onSiteChanged to fix LongMethod detekt finding, remove TODOs
adalpari Mar 18, 2026
e9c6c9d
Fix double Sentry reporting, unreported storeCredentials exception, a…
adalpari Mar 18, 2026
fbdfd15
Restore Sentry reports in helper for storeCredentials false-return paths
adalpari Mar 18, 2026
97659f7
Add commented-out forced error for testing Sentry reporting path
adalpari Mar 18, 2026
b13ba51
Refactor helper to fix LongMethod detekt finding
adalpari Mar 18, 2026
7e6d753
Remove commented-out forced error used for testing Sentry reporting
adalpari Mar 19, 2026
a046bd1
Fix unused imports and missing null annotation from code checks
adalpari Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.SiteActionBuilder
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged
Expand All @@ -22,6 +23,8 @@ import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.Ur
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.UrlUtils
import com.automattic.android.tracks.crashlogging.CrashLogging
import org.wordpress.android.util.crashlogging.sendReportWithTag
import javax.inject.Inject
import javax.inject.Named

Expand All @@ -34,6 +37,7 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
private val siteStore: SiteStore,
private val appPrefsWrapper: AppPrefsWrapper,
private val appLogWrapper: AppLogWrapper,
private val crashLogging: CrashLogging,
) : ViewModel() {
private val _onFinishedEvent = MutableSharedFlow<NavigationActionData>()
/**
Expand Down Expand Up @@ -62,16 +66,12 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
fun setupSite(rawData: String) {
viewModelScope.launch {
if (rawData.isEmpty()) {
appLogWrapper.e(AppLog.T.MAIN, "Cannot store credentials: rawData is empty")
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = "",
oldSitesIDs = oldSitesIDs,
isError = true
)
appLogWrapper.e(
AppLog.T.MAIN,
"A_P: Cannot store credentials: rawData is empty"
)
applicationPasswordLoginHelper.trackStoringFailed("", "empty_raw_data")
emitError(siteUrl = "", errorMessage = "empty_raw_data")
return@launch
}
val urlLogin = applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(rawData)
Expand All @@ -95,7 +95,14 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
try {
applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(urlLogin)
} catch (e: Exception) {
appLogWrapper.e(AppLog.T.DB, "Error storing credentials: ${e.stackTraceToString()}")
appLogWrapper.e(
AppLog.T.DB,
"A_P: Error storing credentials: ${e.stackTraceToString()}"
)
applicationPasswordLoginHelper.trackStoringFailed(
urlLogin.siteUrl, "store_credentials_exception"
)
crashLogging.sendReportWithTag(e, AppLog.T.DB)
false
}
}
Expand All @@ -109,10 +116,21 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
) = withContext(ioDispatcher) {
try {
if (username.isEmpty() || password.isEmpty() || siteUrl.isEmpty() || apiRootUrl.isEmpty()) {
appLogWrapper.e(AppLog.T.MAIN, "Cannot fetch sites for credential storing: " +
"Username: $username, Password: ${password.isEmpty()}, SiteUrl: $siteUrl, " +
"API Root URL: $apiRootUrl")
emitErrorFetching(siteUrl)
appLogWrapper.e(
AppLog.T.MAIN,
"A_P: Cannot fetch sites for credential storing" +
" - username isEmpty=${username.isEmpty()}" +
", password isEmpty=${password.isEmpty()}" +
", siteUrl isEmpty=${siteUrl.isEmpty()}" +
", apiRootUrl isEmpty=${apiRootUrl.isEmpty()}"
)
applicationPasswordLoginHelper.trackStoringFailed(
siteUrl, "empty_fetch_params"
)
emitError(
siteUrl = siteUrl,
errorMessage = "empty_fetch_params"
)
} else {
val xmlRpcEndpoint =
selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(siteUrl)
Expand All @@ -128,61 +146,161 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
)
}
} catch (e: Exception) {
appLogWrapper.e(AppLog.T.API, "Error fetching sites: ${e.stackTraceToString()}")
emitErrorFetching(siteUrl)
appLogWrapper.e(
AppLog.T.API,
"A_P: Error fetching sites: ${e.stackTraceToString()}"
)
applicationPasswordLoginHelper.trackStoringFailed(
siteUrl, "fetch_sites_exception"
)
emitError(siteUrl = siteUrl, errorMessage = e.message, cause = e)
}
}

private suspend fun emitErrorFetching(siteUrl: String) = _onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = siteUrl,
oldSitesIDs = oldSitesIDs,
isError = true
private suspend fun emitError(
siteUrl: String,
errorMessage: String? = null,
cause: Throwable? = null
) {
val exception = cause
?: Exception("Application password login failed: $errorMessage")
crashLogging.sendReportWithTag(exception, AppLog.T.MAIN)
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = siteUrl,
oldSitesIDs = oldSitesIDs,
isError = true,
errorMessage = errorMessage
)
)
)
}

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onSiteChanged(event: OnSiteChanged) {
viewModelScope.launch {
val currentNormalizedUrl = UrlUtils.normalizeUrl(currentUrlLogin?.siteUrl)
val site = siteStore.sites.firstOrNull { UrlUtils.normalizeUrl(it.url) == currentNormalizedUrl }
if (event.rowsAffected < 1 || site == null || applicationPasswordLoginHelper.siteHasBadCredentials(site)) {
appLogWrapper.e(AppLog.T.MAIN, "Site not found or credentials are empty.")
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = currentUrlLogin?.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = true
)
)
if (event.isError) {
handleSiteChangedError(event)
} else {
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = siteStore.hasSite() &&
oldSitesIDs?.contains(site.id) != true, // null or false
showPostSignupInterstitial = !siteStore.hasSite()
&& appPrefsWrapper.shouldShowPostSignupInterstitial,
siteUrl = currentUrlLogin?.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = false,
newSiteLocalId = site.id
)
)
handleSiteChangedSuccess(event)
}
}
}

private suspend fun handleSiteChangedError(event: OnSiteChanged) {
val error = event.error
appLogWrapper.e(
AppLog.T.MAIN,
"A_P: onSiteChanged failed: " +
"SiteStore error ${error?.type}: ${error?.message}"
)
applicationPasswordLoginHelper.trackStoringFailed(
currentUrlLogin?.siteUrl,
"site_changed_failed"
)
emitError(
siteUrl = currentUrlLogin?.siteUrl.orEmpty(),
errorMessage = "site_store_error"
)
}

@Suppress("TooGenericExceptionCaught")
private suspend fun handleSiteChangedSuccess(event: OnSiteChanged) {
val normalizedUrl =
UrlUtils.normalizeUrl(currentUrlLogin?.siteUrl)

val site = try {
siteStore.sites.firstOrNull {
UrlUtils.normalizeUrl(it.url) == normalizedUrl
}
} catch (e: Exception) {
logAndEmitSiteChangedError(
logMessage = "exception reading sites from DB: " +
e.stackTraceToString(),
errorCode = "db_read_exception",
cause = e
)
return
}

val validationError = validateSiteChanged(event, site)
if (validationError != null) {
logAndEmitSiteChangedError(
logMessage = validationError.logMessage,
errorCode = validationError.errorCode
)
} else {
val resolvedSite = site ?: return
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = siteStore.hasSite() &&
oldSitesIDs?.contains(resolvedSite.id) != true,
showPostSignupInterstitial = !siteStore.hasSite()
&& appPrefsWrapper.shouldShowPostSignupInterstitial,
siteUrl = currentUrlLogin?.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = false,
newSiteLocalId = resolvedSite.id
)
)
}
}

private fun validateSiteChanged(
event: OnSiteChanged,
site: SiteModel?
): SiteChangedValidationError? = when {
event.rowsAffected < 1 -> SiteChangedValidationError(
logMessage = "No rows affected " +
"(rowsAffected=${event.rowsAffected})",
errorCode = "no_rows_affected"
)
site == null -> SiteChangedValidationError(
logMessage = "Site not found after update",
errorCode = "site_not_found"
)
applicationPasswordLoginHelper
.siteHasBadCredentials(site) -> SiteChangedValidationError(
logMessage = "Credentials are empty after store",
errorCode = "empty_credentials"
)
else -> null
}

private data class SiteChangedValidationError(
val logMessage: String,
val errorCode: String
)

private suspend fun logAndEmitSiteChangedError(
logMessage: String,
errorCode: String,
cause: Throwable? = null
) {
appLogWrapper.e(
AppLog.T.MAIN,
"A_P: onSiteChanged failed: $logMessage"
)
applicationPasswordLoginHelper.trackStoringFailed(
currentUrlLogin?.siteUrl,
"site_changed_failed"
)
emitError(
siteUrl = currentUrlLogin?.siteUrl.orEmpty(),
errorMessage = errorCode,
cause = cause
)
}

data class NavigationActionData(
val showSiteSelector: Boolean,
val showPostSignupInterstitial: Boolean,
val siteUrl: String?,
val oldSitesIDs: ArrayList<Int>?,
val isError: Boolean,
val newSiteLocalId: Int? = null
val newSiteLocalId: Int? = null,
val errorMessage: String? = null
)
}
Loading
Loading