Flow Lifecycle (UI Layer)#
A UI-Layer integration owns three moments of the flow: starting it, receiving the result, and handling interruption. Everything between those moments — screen rendering, navigation between steps, token refresh — is the framework’s responsibility. This page covers all three for iOS and Android.
For the configuration object the flow runs against, see Configuration .
Starting the Flow#
iOS exposes a single entry point — HaapiFlow.start(...) — taking a presenting view controller and the configured HaapiUIKitApplication. Android exposes HaapiFlowActivity.newIntent(...) and launches it through the Activity Result API.
A complete UIKit pattern — the view controller conforms to HaapiFlowResult, calls HaapiFlow.start(...) on user action, and handles both the success and error outcomes:
extension UIApplication {
var haapiUIKitApplication: HaapiUIKitApplication {
guard let appDelegate = delegate as? AppDelegate else {
fatalError("AppDelegate not configured")
}
return appDelegate.haapiUIKitApplication
}
}
class LoginViewController: UIViewController, HaapiFlowResult {
@IBAction func signInTapped(_ sender: UIButton) {
do {
try HaapiFlow.start(
from: self,
haapiUIKitApplication: UIApplication.shared.haapiUIKitApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared
)
} catch {
// Rare: configuration error prevents flow start
presentAlert("Could not start authentication: \(error)")
}
}
func didReceiveOAuthTokenModel(_ tokenModel: OAuthTokenModel) {
// Persist the tokens (Keychain) and navigate to the authenticated state.
TokenStore.shared.save(tokenModel)
navigateToHome()
}
func didReceiveError(_ error: Error) {
// Flow-level error after the user started authenticating.
presentAlert("Authentication failed: \(error)")
}
}For SwiftUI hosts, the only difference is the presentation site — the conforming view passes itself to HaapiFlow.start from inside .sheet(isPresented:):
struct ContentView: View, HaapiFlowResult {
let haapiApplication: HaapiUIKitApplication
@State private var showingHaapiVC = false
var body: some View {
Button("Sign in") { showingHaapiVC = true }
.sheet(isPresented: $showingHaapiVC) {
HaapiFlow.start(
self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared
)
}
}
func didReceiveOAuthTokenModel(_ tokenModel: OAuthTokenModel) { /* persist + navigate */ }
func didReceiveError(_ error: Error) { /* show alert */ }
}from: — the presenting UIViewController (UIKit) or the conforming view (SwiftUI).
haapiUIKitApplication: — the HaapiUIKitApplication built in AppDelegate from the configured HaapiUIKitConfiguration.
haapiDeepLinkManageable: — HaapiDeepLinkManager.shared handles inbound deep links (universal links / custom schemes) so external-browser-redirect flows complete cleanly.
A complete Activity Result pattern — signInTapped() starts the flow on user action; handleToken() and handleError() are the result-handling counterparts to iOS’s didReceiveOAuthTokenModel / didReceiveError. The launcher property must be declared before the activity reaches STARTED, which is why it lives at class scope rather than inside onCreate:
class MainActivity : AppCompatActivity() {
// Start the flow on user action
private fun signInTapped() {
startHaapiActivityForResult.launch(HaapiFlowActivity.newIntent(this))
}
// Persist the tokens (EncryptedSharedPreferences) and navigate
private fun handleToken(model: OauthModel.Token) {
TokenStore.save(model)
navigateToHome()
}
// Show a user-facing error after a flow-level failure or interruption
private fun handleError(message: String) {
showError(message)
}
// Launcher + result callback — registered at property scope so it is
// ready before the activity reaches STARTED.
private val startHaapiActivityForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { activityResult ->
if (activityResult.resultCode == RESULT_OK) {
when (val model = activityResult.data
?.getParcelableExtra(HaapiFlowActivity.className) as? OauthModel) {
is OauthModel.Token -> handleToken(model)
is OauthModel.Error -> handleError(model.errorDescription ?: "Unknown error")
else -> handleError("Unexpected result")
}
}
if (activityResult.resultCode == RESULT_CANCELED) {
val throwable = activityResult.data
?.getSerializableExtra(HaapiFlowActivity.className) as? Throwable
throwable?.let { handleError("Interrupted: ${it.message}") }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.button).setOnClickListener { signInTapped() }
}
}The legacy startActivityForResult(intent, requestCode) API also works for codebases that haven’t migrated to the Activity Result API; the result-handling switch on OauthModel is identical.
Receiving the Result#
Both platforms deliver one of three outcomes: a successful token model, a flow-level error, or an interruption (user cancelled, system back).
| Outcome | iOS surface | Android surface |
|---|---|---|
| Success | HaapiFlowResult.didReceiveOAuthTokenModel(_ tokenModel: OAuthTokenModel) | OauthModel.Token in the activity result data |
| Flow error | HaapiFlowResult.didReceiveError(_ error: Error) | OauthModel.Error in the activity result data |
| Interrupted | Flow controller dismissed; no didReceive… call | RESULT_CANCELED with an optional Throwable in data |
The token model carries six fields — accessToken, expiresIn, refreshToken, tokenType, scope, idToken — with the same semantics as the SDK-Layer SuccessfulTokenResponse. See OAuthTokenManager for the field-by-field detail and the recommended secure-storage pattern. The framework does not store them for you — once the result handler fires, the framework is done with the OAuth artifacts.
Handling Interruption#
Users dismiss the flow (close button, system back gesture, app backgrounded too long). The framework can either prompt for confirmation or interrupt silently.
- iOS —
shouldConfirmFlowInterruptionon the configuration builder (defaulttrue) controls whether a UIAlert appears before the flow tears down. When the user confirms, the presenting view controller is dismissed;HaapiFlowResultdoes not receive a final callback. When the user cancels the prompt, the flow continues. - Android —
setShouldConfirmInterruptionFlow(true)(default) shows an AlertDialog when the user presses back/close. When the user confirms, the activity finishes withRESULT_CANCELED; otherwise the flow continues.
When the framework auto-interrupts because of a fatal error and shouldAutoHandleFlowErrorFeedback is true (default), an alert is shown first, then the result handler receives the error.
For the actual builder calls that flip these defaults (setShouldConfirmFlowInterruption(false), setShouldAutoHandleFlowErrorFeedback(false), and the rest of the presentation-tuning knobs), see Presentation Options .
Thread / lifecycle constraints when starting the flow:
- iOS — never call
HaapiFlow.startfrom outside the main actor.HaapiUIKitApplicationand the presenting controller are@MainActor-isolated; calling from a background queue results in a runtime crash. Build the application object and start the flow fromAppDelegateor a main-actor-isolated view controller. - Android —
registerForActivityResult(...)must be called before the activity reachesSTARTEDstate (declare it as a property of theAppCompatActivity, not insideonCreateaftersetContentViewreturns the lifecycle to STARTED). Thelaunch(...)call itself must be on the main thread. Wrong-state registration throwsIllegalStateExceptionat runtime; wrong-thread launch throwsCalledFromWrongThreadException.
How to implement this: Configuration · Presentation Options · Error Handling (SDK Layer) · How to Handle Errors