Upgrade — Android UIWidget v5#
The Curity HAAPI Android UIWidget library shipped its v5.0 major release on 2025-11-03, followed by minor releases through 5.6.0 (2026-08-10). This page covers the migration from v4.x to v5.x and highlights the notable additions in each 5.x release.
For the authoritative per-release detail, see the changelog on curity.io/docs/haapi-android-ui/latest/CHANGELOG.html.
Breaking Changes in v5.0#
Kotlin Toolchain#
The framework is now compiled with Kotlin 2.2.20. Backward compatibility is maintained at the Kotlin 2.0 API / language level, so the library remains usable from projects on Kotlin 1.9+.
IdsvrHaapiException Hierarchy (Driver Layer)#
The Driver Layer (consumed transitively by UIWidget) introduced a sealed IdsvrHaapiException hierarchy with two categories:
IdsvrHaapiException.Retryable— transient errors that can be retried automaticallyIdsvrHaapiException.Unrecoverable— permanent errors requiring intervention
Code that previously caught the older HaapiException types must be re-mapped onto these categories. See Error Handling for the per-category dispatch.
HaapiLogger API (Driver Layer)#
Driver v5.0 removed the deprecated HaapiLogger surfaces:
- Static logging-level configuration properties — replaced by the level setter that takes a
LogLevel. - Logging methods taking
tag: Stringas the first argument — replaced by level-specific calls that takesender: Class<*>.
See Upgrade — HaapiLogger setLogType Migration.
OAuth Error Model#
OauthModel.Error was updated for RFC 6749 compliance (visible through the SDK Layer):
errorDescriptionis now optionalerrorUri(optional) has been added
Code that force-reads errorDescription must guard against null.
WebAuthn — ExperimentalWebAuthnApi Removed#
The ExperimentalWebAuthnApi opt-in annotation is gone. WebAuthn methods and properties on WebAuthnRegistrationClientOperationStep and WebAuthnAuthenticationClientOperationStep are now part of the stable API. Remove any @OptIn(ExperimentalWebAuthnApi::class) annotations from your code.
The deprecated WebAuthnRegistrationClientOperationStep.buildParameters(attachment: Attachment, …) overload was removed. Use the overload that takes publicKey: ClientOperationActionModel.WebAuthnRegistration.PublicKey as the first argument instead.
OAuthTokenManager Construction (SDK Layer)#
The OAuthTokenManager direct constructor is removed. Build instances through HaapiAccessorFactory.createForOAuth(…) (or, for the HAAPI flow, createForHaapi(…)). See Creating a HaapiAccessor.
Breaking Changes in v5.1#
HandleableProblemType gained a new case: TooManyAttempts. This is a source-breaking change only if you when on HandleableProblemType without an else branch.
when (problemType) {
HandleableProblemType.InvalidInputProblem -> …
HandleableProblemType.TooManyAttempts -> … // ← new in 5.1
else -> …
}
IdsvrHaapiException was made Parcelable in v5.1 (along with OAuthUnrecoverableException in the SDK). If your app extends from these types, the Parcelable contract is now required.
Upgrade to 5.1.0 if you’re on 5.0.0. v5.0.0 crashes when handling the new exception hierarchy under parceling. v5.1.0 fixes it.
Breaking Changes in v5.5#
LinkItemModel gained a rel: String property, forwarded from Link.rel. The property has no default, so a custom LinkItemModel implementation does not compile until it supplies one:
class MyLinkItem(
override val text: CharSequence?,
override val href: String,
override val rel: String, // ← required from 5.5
override val type: String?
) : LinkItemModel
Apps that use the built-in models and mappers are unaffected. UIModel.Problem.iss was added in the same release but defaults to null, so it needs no change in custom implementations.
Breaking Changes in v5.6#
ui.widget Publishes Two Variants#
ui.widget now publishes a debug and a release variant under the same coordinate, so the HaapiUI Previewer reaches debug builds and is structurally absent from release builds. Two consequences:
-
Custom build types must declare a fallback. With a build type other than
debug/release, Gradle cannot decide which variant to resolve and the build fails withNo matching variant … BuildTypeAttr. Add onematchingFallbacksline per custom build type —['release']for release-like types (no previewer) or['debug']to opt the previewer in:// app/build.gradle android { buildTypes { staging { initWith debug; matchingFallbacks = ['release'] } // release-like: no previewer internalDebug { initWith debug; matchingFallbacks = ['debug'] } // debug-like: previewer included } } -
Consumption requires Gradle Module Metadata — Gradle / AGP, the Android default. Resolving
ui.widgetwith plain Maven is no longer supported.
Standard debug / release projects keep their single implementation line and need no change. Consumers of only sdk or driver are unaffected. See Preview Tools.
Token-Binding Paths Are Mutually Exclusive#
The legacy TokenBoundConfiguration path and the new dpopTokenBinding path cannot be combined. Supplying both is rejected immediately with an IllegalArgumentException at configuration time rather than silently preferring one. This applies at every layer — WidgetConfiguration.Builder, HaapiConfiguration, and HaapiTokenManager. Migration is covered in the next section.
Migrating Token Binding to the DPoP Builder (v5.6)#
DPoP token binding is now configured with DpopTokenBindingConfiguration.Builder(context), which defaults to a hardware-backed key and a built-in encrypted store. Implementing Storage yourself is no longer part of enabling token binding. The legacy TokenBoundConfiguration path is deprecated at every layer; it stays functional throughout 5.x and is slated for removal in a future major.
Swap the Setter#
The migration is one call site per configuration. UIWidget:
// Before (5.5 and earlier)
WidgetConfiguration.Builder(/* … */)
.setTokenBoundConfiguration(
TokenBoundConfiguration(storage = myStorage /* … */)
)
.build()
// After (5.6)
WidgetConfiguration.Builder(/* … */)
.setDpopTokenBinding(
DpopTokenBindingConfiguration.Builder(context).build()
)
.build()
The same configuration object is accepted by the lower layers — HaapiConfiguration(dpopTokenBinding = …) on the SDK Layer and HaapiTokenManager.Builder.setDpopTokenBinding(…) on the Driver Layer.
Established Sessions Do Not Re-Authenticate#
An existing binding is adopted in place, so a user who is already signed in stays signed in:
-
A key stored under the manager’s
keyStoreAliasis adopted automatically — nothing to configure. -
A key stored under a custom legacy alias is adopted by naming it on the builder:
DpopTokenBindingConfiguration.Builder(context) { migrateFromLegacyAlias = "my-legacy-dpop-alias" }.build()
Choose Key Protection Deliberately#
keyType takes a DpopKeyPolicy: Auto (the default — hardware-backed, with a transparent fallback to an encrypted software key), Hardware (fails closed with a typed error when no usable Keystore is present), or Software. Because Auto can fall back, read the protection actually in use rather than assuming it:
val protection = haapiManager.dpopKeyInUse // Hardware, Software, or None
When useAttestation is true, attestation requires a hardware-backed key and keyType is not consulted.
Storage Is Provided#
storage defaults to StorageBuilder.encrypted(context) — AES-GCM at rest under a non-exportable Keystore key. Use StorageBuilder.shared(context) when a separate process or an app widget must read the same session. A custom Storage is still accepted, and must encrypt and integrity-protect values at rest, because stored values include serialized key material.
See How to Configure Token Binding, Token Binding (SDK Layer), and Token Binding (Driver Layer).
Notable Additions Across 5.x#
5.1 (2025-12-15)#
WidgetConfiguration.Builder.setUsePasskeysBrowserFallback— configures behavior when Passkeys are not supported on the device.- Improved handling for
Too many attemptsandIncorrect Credentialsproblems inFormFragment. - WebAuthn / Passkeys now uses server-provided
residentKeyandrequireResidentKeyvalues.
5.2 (2026-02-09)#
- Polling cancellation no longer triggers on Intent dispatch.
- Crashes during polling transitions are fixed.
- Driver Layer adds
HttpClientRetryableExceptionspecializations (SocketStreamInterruptionException,HostConnectionException,HttpRetryException) for cleaner retryable-error classification. - SDK Layer adds an automatic single-retry for
HttpClientRetryableExceptioninHaapiManagerandOAuthTokenManager.
5.3 (2026-03-23)#
BankIdFragmentaligned with latest accessibility requirements. NewBankIdModelsupports the new BankID flow.representationTypeonUIModel.Interaction,UIModel.Problem,UIModel.Operation— exposes the originating HAAPI representation type.InteractionValueinterface — exposesisRequired, defaulting totrue. Custom implementations can override.InteractionErrorinterface for items carrying an error.FormFragmentrenders a required-field asterisk on text and select fields during signup, using the configurederrorColor.- SDK Layer adds optional
minLength/maxLengthtoFormField.Text,FormField.Username,FormField.Password. - Logging migration —
setLogType()introduced; staticisXxxEnabledflags are deprecated. See Upgrade — HaapiLogger setLogType Migration.
5.4 (2026-05-04)#
- HaapiUI Previewer — debug-only tool to iterate on
Theme.Haapi.Ui.Widget.BaseThemeattributes without running a full HAAPI flow. Two surfaces:- Compose
@PreviewviaHaapiUIPreviewerfor themed screen layouts and component galleries in Android Studio’s preview pane. - On-device previewer Activity via
HaapiUIPreviewerHostActivity(andHaapiUIPreviewerScenarioListActivity) running real Fragments through the production pipeline. - Previewer code lives in
src/debug/and is excluded from release AARs. Public types are marked@ExperimentalHaapiApiand may change in minor releases.
- Compose
userCodemessage style for recovery codes (themed 2-column grid with copy action).PollingFragmentandBankIdFragmenthonor the server-provided polling interval fromProperties.Polling.interval.- SDK Layer adds the optional
intervalonProperties.Polling.
5.5 (2026-06-15)#
UIModel.Problem.iss— optional issuer identifier of the authorization server, forwarded fromAuthorizationProblemfor OAuth issuer verification (RFC 9207). Reachable from custom mappers and Fragments; the built-inProblemFragmentdoes not render it. Defaults tonull, so custom implementations need no source change.LinkItemModel.rel— the link relation, forwarded fromLink.rel, for relation-based routing in custom mappers and Fragments. CustomLinkItemModelimplementations must supply it — see Breaking Changes in v5.5 above.- BankID same-device and cross-device flows are handled as separate server responses, with the legacy combined response still supported. Legacy BankID v5 protocol handling was removed.
- QR code decoding runs off the main thread, so it no longer blocks BankID polling UI refreshes.
- Widget component style attributes no longer leak across style variants.
- SDK Layer adds the optional
issonAuthorizationProblem.
5.5.1 (2026-06-25)#
- The Driver Layer’s debug
network_security_config.xml— which permitted cleartext traffic to development and LAN hosts — no longer ships in the released artifact. Before this patch, an app depending on UIWidget inheritedandroid:networkSecurityConfigwithcleartextTrafficPermitted="true"in its merged manifest through the transitive driver dependency. Upgrade to 5.5.1 or later on any 5.x line, and check your app’s merged manifest to confirm the attribute is gone.
5.6 (2026-08-10)#
- DPoP token binding is configured with a builder.
DpopTokenBindingConfiguration.Builder(context)replaces hand-writtenTokenBoundConfigurationplus a customStorage, defaulting to a hardware-backed key and an encrypted store. Reachable from UIWidget viaWidgetConfiguration.Builder.setDpopTokenBinding(…), from the SDK Layer viaHaapiConfiguration.dpopTokenBinding, and from the Driver Layer viaHaapiTokenManager.Builder.setDpopTokenBinding(…). See Migrating Token Binding to the DPoP Builder above.DpopKeyPolicyselects key protection —Auto(default),Hardware, orSoftware— anddpopKeyInUsereports the protection actually in use (Hardware,Software, orNone).StorageBuilder.encrypted(context)andStorageBuilder.shared(context)provide ready-made encrypted stores;sharedis readable across the app’s processes and components.- The legacy path is deprecated and mutually exclusive with the new one — see Breaking Changes in v5.6 above.
- The HaapiUI Previewer reaches consumer apps. The previewer added in 5.4 was published from the release variant only, so it never actually shipped to consumers. From 5.6.0 the
ui.widgetartifact publishes both build variants under the same coordinate: a debug build receives the previewer automatically, with no new dependencies, and release builds structurally exclude it. Standarddebug/releaseprojects need no configuration; custom build types need onematchingFallbacksline — see Breaking Changes in v5.6 above and Preview Tools. The previewer API remains@ExperimentalHaapiApiand may change in minor releases. - SDK Layer — DPoP token binding uses a distinct key per
HaapiManager, derived from that manager’skeyStoreAlias, so multiple concurrent accounts (one manager each) get independent keys that never collide. (Superseded in 5.7 — one manager now supports several concurrent sign-ins, each with its own key. See below.) - SDK Layer — revoking an access token no longer deletes the token-bound key, so a following
refreshAccessTokensucceeds instead of failing withinvalid_dpop_proof. The key is removed only on a refresh-token revoke or aninvalid_grantresponse. - The compiler-generated
copy()on data classes with aninternalprimary constructor is nowinternalas well, matching the constructor. These types were never intended to be constructed or copied by consumers. Destructuring (componentN()) is unaffected.
5.7 (unreleased)#
- SDK Layer —
OAuthTokenManager.dpopHelper(accessToken)returns theDpopHelperholding the DPoP key that access token is bound to, so a DPoP-bound access token can be presented to a resource server: sign a proof with it and send that in theDPoPheader alongsideAuthorization: DPoP <accessToken>. It is asuspendfunction because the key is read from the Keystore.nullis a read miss — no DPoP binding, or a token this client did not issue — while a Keystore that could not be read throwsIdsvrHaapiExceptioncarrying the cause, so a possibly transient failure is not mistaken for an absent key. UIWidget integrators reach the same lookup throughOAuthLifecycle.dpopHelper(widgetConfiguration, accessToken), which builds an accessor per call. The resource server’s nonce stays the application’s to manage: the nonce the SDK tracks belongs to the authorization server. See OAuthTokenManager. - A signed-in user stays signed in. Refreshing a token-bound sign-in previously failed with
invalid_grant— “Invalid cnf/jkt claim in refresh token” — after anything started a new authentication flow: another sign-in, a step-up, an app restart followed bystart(), or the HAAPI token expiring. One sign-in was enough to hit it, and the user was logged out. Refreshing now keeps working. If you have seen that error, or unexplained repeated logouts on a token-bound client, this is the fix. - Upgrading does not log anyone out. Sign-ins established on any earlier 5.x or 4.x version keep refreshing after the update, with no re-authentication, however token binding was configured.
- One
HaapiManagernow supports several concurrent sign-ins. Each refreshes and is revoked independently, and ending one no longer affects the others. A manager per account still works and remains the way to keep accounts fully isolated, but it is no longer required to hold more than one live sign-in. invalid_grantaffects only the rejected sign-in. The user’s other sign-ins keep working.- Downgrading below 5.7 is not supported for sign-ins established on it. An older SDK cannot use them, and those users would have to sign in again. Roll forward rather than back.
- No change to the proofs on the wire. The Android Driver has always written the
athclaim whenever a proof is created for an access token, as RFC 9449 §4.2 requires, so nothing about DPoP proofs changes in this release. The equivalent iOS release does carry such a change — if you maintain both platforms, that entry in the iOS guide does not apply here.
Related Driver / SDK Migrations#
UIWidget composes the Android SDK and Driver layers. The following lower-layer changes affect host apps that touch those types directly:
- DPoP Nonce Auto-Management —
DPoPNonceStackdirect methods are deprecated in Driver v5.3; the SDK manages the nonce internally. - HaapiLogger setLogType Migration — logging API migration (v5.3).
- The v5.6 DPoP token-binding migration above applies to the SDK and Driver layers as well:
HaapiConfiguration.tokenBoundConfigurationandHaapiTokenManager.Builder.setTokenBoundConfiguration(…)are deprecated alongside the UIWidget setter.
Backward Compatibility#
HaapiAccessorFactory.createwas deprecated in v5.1 in favor ofcreateForHaapi/createForOAuth. The old method still compiles in 5.x.DPoPNonceStack.pop()/push(value)/peek()are deprecated in favor ofget()/set(value)/clear()in Driver v5.3. The class itself is deprecated for removal in a future major.HaapiConfigurable.dPoPNonceStackandHaapiConfiguration.dPoPNonceStackare deprecated in SDK v5.3.TokenBoundConfigurationand everysetTokenBoundConfiguration(…)/tokenBoundConfigurationslot are deprecated in v5.6 in favor ofDpopTokenBindingConfiguration.BuilderandsetDpopTokenBinding(…)/dpopTokenBinding. The legacy path still compiles throughout 5.x, but it cannot be combined with the new one.