HAAPI Flow#
HaapiManager exposes the HAAPI flow as a structured state machine — start begins the flow, and submit / followLink advance it. Each step is returned as a typed model that your UI renders: a form to display, a selector to choose from, a polling indicator, a problem screen, or a final authorization response.
Starting the Flow#
HaapiManager.start triggers the first server interaction and returns the initial step:
haapiManager.start { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
} GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val response = haapiManager.start(coroutineContext)
handleHaapiResponse(response)
} const response = await accessor.haapiManager.start()
handleHaapiResponse(response)Transport-level failures, attestation failures, and bridge invariants reject the Promise with a typed HaapiError. Wrap in try/catch + isHaapiError(e) when you need to recover. See Error Handling .
The result falls into one of three top-level categories:
- A representation — a typed step model the UI maps to a screen.
- A problem representation — an error that requires the user to correct input and retry.
- An error — a framework or transport failure (iOS surfaces this as a result variant; Android raises it as an exception).
Handling Step Types#
The step model is a sealed hierarchy. Cast or match on the concrete type to dispatch to the right UI:
private func handleHaapiResult(_ haapiResult: HaapiResult) {
switch haapiResult {
case .representation(let haapiRepresentation):
handleHaapiRepresentation(haapiRepresentation)
case .problem(let problemRepresentation):
handleProblemRepresentation(problemRepresentation)
case .error(let anError):
handleError(anError)
}
}
private func handleHaapiRepresentation(_ haapiRepresentation: HaapiRepresentation) {
switch haapiRepresentation {
case let authenticatorSelectorStep as AuthenticatorSelectorStep:
// render authenticator selector
break
case let interactiveFormStep as InteractiveFormStep:
// render form
break
case let pollingStep as PollingStep:
// start polling
break
case let authResponseStep as OAuthAuthorizationResponseStep:
// flow complete — exchange code for tokens
break
default:
// handle other step types
break
}
}All 14 step types and their carried fields are catalogued in iOS Representations .
private fun handleHaapiResponse(haapiResponse: HaapiResponse) {
when (haapiResponse) {
is HaapiRepresentation -> handleHaapiRepresentation(haapiResponse)
is ClientOperationStep -> handleClientOperationStep(haapiResponse)
is ProblemRepresentation -> handleProblemRepresentation(haapiResponse)
}
}
private fun handleHaapiRepresentation(haapiRepresentation: HaapiRepresentation) {
when (haapiRepresentation) {
is AuthenticatorSelectorStep -> { /* render authenticator selector */ }
is InteractiveFormStep -> { /* render form */ }
is PollingStep -> { /* start polling */ }
is OAuthAuthorizationResponseStep -> { /* flow complete — exchange code for tokens */ }
else -> {
// handle other step types
}
}
}All 14 step types and their carried fields are catalogued in Android Representations .
HaapiResponse is a discriminated union on responseCategory; the representation it carries is a discriminated union on stepType. Branch on both, narrowing TypeScript at each step:
import { StepType } from 'identityserver.haapi.reactnative.sdk'
import type { HaapiResponse } from 'identityserver.haapi.reactnative.sdk'
function handleHaapiResponse(response: HaapiResponse) {
if (response.responseCategory === 'problem') {
// RFC 7807 problem — see "Problems" in React Native Representations
handleProblem(response.problem)
return
}
switch (response.representation.stepType) {
case StepType.AuthenticatorSelector:
// narrows to AuthenticatorSelectorStep
break
case StepType.InteractiveForm:
// narrows to InteractiveFormStep
break
case StepType.Polling:
// narrows to PollingStep
break
case StepType.OAuthAuthorizationResponse:
// narrows to OAuthAuthorizationResponseStep — exchange code for tokens
break
case StepType.ExternalBrowserClientOperation:
case StepType.BankIdClientOperation:
case StepType.WebAuthnRegistrationClientOperation:
// Client-operation steps — delegate to the host platform
break
default:
// GenericRepresentationStep, ContinueSameStep, UserConsentStep, RedirectionStep, etc.
break
}
}All 14 step types and their carried fields are catalogued in React Native Representations . Unlike iOS / Android, React Native does not surface client-operation steps as a separate sibling category — they live alongside other representations under the same representation field, discriminated by stepType.
Common step types across platforms include AuthenticatorSelectorStep, InteractiveFormStep, PollingStep, RedirectionStep, UserConsentStep, ContinueSameStep, OAuthAuthorizationResponseStep, and GenericRepresentationStep. The Android SDK additionally surfaces ClientOperationStep (and its sub-types ExternalBrowserClientOperationStep, WebAuthnRegistrationClientOperationStep, and others) as a sibling category for steps that delegate to an external operation rather than rendering a screen.
Advancing the Flow#
Based on user interaction, call either submit (when the step represents a form the user filled in) or followLink (when the step exposes navigation links the user chose). The next step is delivered the same way as start:
private func submit(formActionModel: FormActionModel, parameters: [String: Any]) {
haapiManager.submitForm(formActionModel, parameters: parameters) { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
}
}
private func followLink(_ link: Link) {
haapiManager.followLink(link) { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
}
} private fun submit(form: FormActionModel, parameters: Map<String, Any>) {
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val response = haapiManager.submitForm(
form = form,
parameters = parameters,
onCoroutineContext = this.coroutineContext
)
handleHaapiResponse(response)
}
}
private fun followLink(link: Link) {
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val response = haapiManager.followLink(
link = link,
onCoroutineContext = this.coroutineContext
)
handleHaapiResponse(response)
}
} Pass the entire FormAction (or Link) from the current step — not a copy or reconstructed object. The SDK identifies the target action by reference; cached actions from a previous accessor lifecycle yield HaapiErrorCode.FlowCacheMiss.
import type { FormAction, Link } from 'identityserver.haapi.reactnative.sdk'
async function submit(action: FormAction, parameters: Record<string, unknown>) {
const response = await accessor.haapiManager.submitForm(action, parameters)
handleHaapiResponse(response)
}
async function follow(link: Link) {
const response = await accessor.haapiManager.followLink(link)
handleHaapiResponse(response)
} Parameter values must be JSON-serialisable on all three platforms — string, number / Int / Long / Double, boolean, null, nested objects, arrays. The native side validates the type set at runtime; supplying a non-JSON-serialisable value (a class instance, a Date, a function) raises IllegalArgumentException on Android, throws on iOS, and rejects the Promise as HAAPI_UNEXPECTED on React Native.
For GET form actions, all three platforms require string-only parameter values. Convert numbers, booleans, and dates to String before submitting GET forms.
Polling Intervals#
When handling a PollingStep, check pollingStep.pollingProperties.interval for a server-provided polling interval (milliseconds, represented as a string). When present, prefer the server value over any client-side default. For BankID flows, the precedence is: server interval → BankID’s published 1-second cadence → application default.
Reaching the End of the Flow#
The flow completes when HaapiManager returns an OAuthAuthorizationResponseStep. The step’s properties contain a code — exchange it for tokens via OAuthTokenManager.fetchAccessToken. See OAuthTokenManager .
How to implement this: iOS SDK · Android SDK · OAuthTokenManager · Error Handling