Skip to content

fix: [ANDROAPP-7759] Report DomainError as a failed Result in the PIN use cases - #5080

Draft
andresmr wants to merge 1 commit into
developfrom
claude/androapp-7759-draft-pr-25b88e
Draft

andresmr wants to merge 1 commit into
developfrom
claude/androapp-7759-draft-pr-25b88e

Conversation

@andresmr

@andresmr andresmr commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes ANDROAPP-7759

Problem

DomainError is sealed class DomainError : Throwable(), so the catch (e: Exception) in
ForgotPinUseCase and SavePinUseCase never matched. A mapped SDK error propagated out of the
use case instead of becoming Result.failure(...) as the UseCase contract promises — and since
PinViewModel calls them inside viewModelScope.launch and only handles onSuccess / onFailure,
"Forgot PIN" against a failing SDK crashed the app instead of showing a message, leaving the
spinner up.

Independently, CancellationException is an Exception, so both use cases were swallowing
cancellation and reporting it as a failed Result.

Changes

  • New resultOf { } helper in commonskmm/commonMain (org.dhis2.mobile.commons.domain),
    mirroring the existing withDomainErrors: catches Throwable so a DomainError is reported as
    a failure, and rethrows CancellationException so cancellation is never a failure. The stdlib
    runCatching is not a substitute — it swallows cancellation, which is why the idiom was already
    hand-rolled in FetchOrgUnits.kt and LoginRepositoryImpl.kt.
  • Both PIN use cases run on that helper. SavePinUseCase now implements UseCase<String, Unit>
    as AGENTS.md requires; the signature already matched, so callers are unaffected.
  • SessionRepositoryImpl moves its four inline catch (d2Error: D2Error) blocks onto
    withDomainErrors. This closes a live gap in the same file: those inline catches only saw a bare
    D2Error, so any failure the SDK's blocking RxJava operators rewrapped in a RuntimeException
    escaped savePin / deletePin / logout completely unmapped — the same bug class fixed for
    sync in refactor: [ANDROAPP-7733] Map SDK errors in sync repositories with withDomainErrors #5071. The no-op catch (e: Exception) { throw e } in setSessionLocked goes with it.

Tests

ResultOfTest (commonskmm commonTest), extended ForgotPinUseCaseTest / SavePinUseCaseTest
(login commonTest) and a new SessionRepositoryImplTest (login androidHostTest) prove that:

  • a DomainError thrown by the repository comes back as Result.failure carrying the same instance;
  • a SessionRenewalRequiredError from logout() survives as itself, so the session renewal dialog can still fire from this path;
  • CancellationException propagates instead of becoming a failed Result;
  • at the repository level, RuntimeException(d2Error) maps as well as a bare D2Error, and an unrelated failure travels on untouched.

./gradlew :commonskmm:testAndroidHostTest :login:testAndroidHostTest → 201 passed, 0 failed.
./gradlew ktlintCheck → clean. :app:compileDhis2DebugKotlin → clean.

Deliberately out of scope

Two follow-ups the ticket calls out, each wanting its own ticket:

  • Making DomainError extend Exception would repair every site at once, but has a measured blast
    radius of 42 catch (…: Exception) sites in the KMP modules; LoginRepositoryImpl.kt:136
    and :154 would silently downgrade a precise error such as SessionRenewalRequiredError to a
    generic ConfigurationError.
  • PinViewModel uses viewModelScope.launch instead of launchUseCase, so its coroutines are not
    tracked by the Espresso idling resource.

🤖 Generated with Claude Code

… use cases

DomainError extends Throwable, so the catch (e: Exception) in ForgotPinUseCase and
SavePinUseCase never matched: a mapped SDK error escaped the use case instead of
becoming Result.failure, and PinViewModel runs them in viewModelScope.launch, so
"Forgot PIN" against a failing SDK crashed the app rather than showing a message.

Both use cases now run on a shared resultOf { } helper in commonskmm, which catches
Throwable and rethrows CancellationException -- the stdlib runCatching swallows it,
which is why it is not a substitute. SavePinUseCase also implements UseCase<String, Unit>,
as AGENTS.md requires; the signature already matched, so callers are unaffected.

SessionRepositoryImpl moves its four inline catch (d2Error: D2Error) blocks onto
withDomainErrors, closing the same gap fixed for sync in #5071: those catches only saw
a bare D2Error, so any failure the SDK's blocking RxJava operators rewrapped in a
RuntimeException escaped savePin / deletePin / logout completely unmapped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant