Inquiry UI

What an inquiry is, how it runs from the launch code your backend creates to the saved identity record, and the ready-made inquiry UI on iOS, Android and the web.

Preview

This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.

An inquiry is one identity verification of one person. Folio defines what the inquiry asks for; the SDK runs it with InquiryStore, and the ready-made inquiry UI renders it. The UI shows each step, runs the camera, the NFC reader and the selfie engine, and sends every interaction of the person to the store. All decisions, such as which step comes next or where an error belongs, stay in the SDK. Your app starts the inquiry with a launch code and watches its lifecycle.

This page is the overview. The details are on these pages:

PageCovers
Inquiry storeInquiryStore: entries, actions, intents, state, stages and errors in the model
Inquiry embedThe script-tag loader for web pages that do not run the SDK
NFC chip readingMrtdStore and the chip reading step
Selfie captureThe selfie step and the selfie capture providers
Identity recordsIdentityRecordMapper and how a verified identity is saved
ErrorsInquiryError, InquiryErrorView and the embed errors

The steps of an inquiry

The inquiry defines which steps the person sees and in which order. Folio sends every text already translated into the person's language, so your app configures neither the steps nor their text. The store shows a step as one of these stages:

StageWhat the person does
ScreenReads text and fills in fields: text, numbers, dates, addresses, phone numbers, countries, selections, checkboxes and files. A screen can carry a language selector.
VerificationGoes through one verification step. See the next table.
HandoffMoves the inquiry to another device with a code shown as a QR code or a link, or sent by email or text message. The other device takes control with the continue button of its observer screen; see Control of the inquiry.
TerminalReaches the end of the inquiry.

The verification steps and their screens:

StepScreensWhat the person does
GovernmentIdCountry, Template, Scan, Confirm, UploadReview, ErrorPicks the country and the document, takes photos of the document with the camera or picks a file, and confirms them.
GovernmentIdNfcInfo, Details, Scan, ErrorHolds the passport or ID card to the phone so that the chip can be read. See NFC chip reading.
SelfiePreparing, Intro, Liveness, Passive, Photo, Review, ErrorTakes a selfie with a liveness check, or a photo: with the front camera on iOS and the web, with the system image picker on Android. See Selfie capture.
EmailOtpBound, Verify, Confirm, ErrorEnters an email address, or confirms sending the code to the one the inquiry already holds, then enters the one-time code sent to it.
PhoneOtpBound, Verify, Confirm, ErrorEnters a phone number, or confirms sending the code to the one the inquiry already holds, then enters the one-time code sent to it.

How it works

  1. Your backend creates the inquiry. It receives a one-time launch code. The SDK never creates an inquiry. The launch code is single-use: mint a fresh one for each inquiry, and keep the credentials your backend uses with Folio on your backend.
  2. Your backend passes the launch code to your app.
  3. Your app opens the inquiry. It mounts an InquiryStore on its FolioSdk runtime, sends Open with the entry Start and the launch code, and hands the store to the ready-made UI. The SDK exchanges the launch code with Folio for the inquiry. On Android FolioInquiry.open and on the web openInquiry mount the store and send Open in one call. The ready-made UI itself never sends Open.
  4. The inquiry starts. The lifecycle is Ready while the store waits for the inquiry, then Started with a correlationId, the inquiry id. Use it to tie your logs to the inquiry.
  5. The person goes through the steps. The UI renders each stage and sends one intent per interaction. The SDK submits the steps to Folio.
  6. The verified identity is saved. When a government ID verification succeeds, the SDK calls your IdentityRecordMapper with the VerifiedIdentity and saves the RecordContent it returns in the vault. The identity is also in InquiryUiModel.identity. Selfie and one-time code results never reach the mapper. See Identity records.
  7. The inquiry ends. While the Terminal stage shows, the lifecycle stays Started. Once the store has finished loading, the UI opens the stage's redirect, when there is one, and sends Close. The redirect is a Universal link target: the web UI navigates the same tab to it, and on iOS and Android the system opens it with its universal-link handling. After that Close the lifecycle becomes Closed with the outcome. When the person leaves before the end, the UI sends Close and the lifecycle becomes Closed without an outcome, unless the inquiry had already reached one. An inquiry that issues a document ends on its terminal stage like any other; the inquiry shows no document. The SDK receives the document into the vault on its own, and FolioDocumentListStore shows it; see Folio documents.
  8. Your app cleans up. On Closed, your app dismisses the UI, if the entry point does not, and closes the store. The ready-made UI never closes the store you pass to it, on any platform, and sends nothing after its own Close until your app sends a new Open on the store.

Closed comes only after a Close: the inquiry reaching a final status does not close it by itself, and neither does Failed. Close ends the inquiry on this device only. It does not cancel the inquiry at Folio; the Cancel intent of the inquiry does.

The SDK validates and normalizes every link of a step and the terminal redirect before the UI sees them: HTTPS://Example.COM becomes https://example.com/, and a space becomes %20. A URL that does not parse fails with InvalidLink. See Capture modes and links.

Entries

The entry tells the store which inquiry to open:

EntrySwiftKotlinTypeScript
Start with a launch code.start(launchCode: launchCode)InquiryEntry.Start(launchCode)InquiryEntry.start(launchCode)
Resume on this device.resume(source: .sameDevice(inquiryId: id))InquiryEntry.Resume(ResumeSource.SameDevice(id))InquiryEntry.resume(ResumeSource.sameDevice(id))
Continue from another device.resume(source: .anotherDevice(code: code))InquiryEntry.Resume(ResumeSource.AnotherDevice(code))InquiryEntry.resume(ResumeSource.anotherDevice(code))

A same-device resume without an inquiryId (nil in Swift, null in Kotlin, undefined in TypeScript) resumes the current inquiry this device saved. A same-device resume with an inquiryId this device has not saved fails with NoActiveInquiry. The code of another device comes from a Handoff stage on that device. See Inquiry store.

The ready-made UI

PlatformEntry pointsPackage
iOSThe SwiftUI view FolioInquiry(store:platform:), with an InquiryPlatform(nfc:selfieCapture:). In UIKit, host it in a UIHostingController.FolioInquiryUI
AndroidFolioInquiry.open(runtime, entry) to mount the store and open the entry. FolioInquiry.Host(store, platform) in Compose, FolioInquiry.present(activity, store, platform) in an activity, with an InquiryPlatform(nfc, selfieCapture).id.folio:sdk, package id.folio.sdk.inquiry
WebopenInquiry(runtime, entry) to mount the store and open the entry, and the React component FolioInquiry with the props store and platform.@folio/sdk/inquiry

The store is mounted on your FolioSdk. The platform object holds what the flow needs from the device: on iOS and Android the runtime that reads the NFC chip, your FolioSdk, and the selfie capture provider; on the web the way links open and the URL of the shipped assets.

The colors, fonts and sizes of the UI come from FolioSdk.inquiryTheme() on every platform; your app does not configure them. On the web, FolioInquiry installs them as CSS variables when it mounts: see Web setup.

On iOS, FolioInquiry renders the store you mounted and opened. Remove the view and close the store when the lifecycle becomes closed.

Swift
let store = try InquiryStore.mount(runtime: sdk)
try store.dispatch(action: .open(entry: .start(launchCode: launchCode)))
lifecycle = store.$state
    .map(\.lifecycle)
    .sink { lifecycle in
        if case .closed = lifecycle { finish() }
    }
let platform = InquiryPlatform(nfc: sdk)
let flow = UIHostingController(rootView: FolioInquiry(store: store, platform: platform))

See iOS setup.

Pages without the SDK

The embed loader runs the inquiry on any web page, without npm, a bundler or React. The inquiry runs in an iframe served from Folio's origin, with its own copy of the SDK. Your page loads one module script and mounts the inquiry with the launch code:

index.html
<div id="folio-inquiry"></div>
<script type="module" src="<embed-loader-url>"></script>
<script type="module">
    const handle = window.FolioInquiry.mount({
        container: '#folio-inquiry',
        app: { version: '2.3.4', build: '567' },
        mode: 'built-in',
        backdrop: { color: 'rgba(0, 0, 0, 0.5)' },
        panel: { background: '#ffffff', shadow: '0 12px 48px rgba(0, 0, 0, 0.24)' },
        entry: { type: 'START', value: { launchCode: backendMintedLaunchCode } },
        onLifecycle(lifecycle) {
            if (lifecycle.type === 'CLOSED') handle.destroy();
        },
    });
</script>

backendMintedLaunchCode stands for the launch code your backend passed to the page, and <embed-loader-url> for the loader URL Folio gives you together with the list of your page origins it accepts. The loader sends the launch code to the iframe only after the iframe has reported READY and passed the version check. backdrop and panel are required in every mode: they color the overlay and the panel of the modal. The mode is 'modal' by default, which shows the inquiry in an overlay that removes itself when the lifecycle reaches CLOSED. See Inquiry embed for the options, the handle, the Content Security Policy and the embed's own errors.

Lifecycle

InquiryUiModel.lifecycle reports the progress of the inquiry: Ready while the store waits, Started while the inquiry runs, including while its Terminal stage shows, Closed with an optional TerminalOutcome after the UI sent Close, and Failed with the raw InquiryError when Open failed before the inquiry started or saving the verified identity failed. Failed is informational: the UI shows the failure notice, and the person leaves through its close button, which leads to Closed. Lifecycle lists the fields and the spelling on each platform, and Outcomes the final statuses.

The embed passes the same values to onLifecycle. It also reports FAILED when the embed itself fails, with an EMBED or BOOT_FAILED error that never appears in a store's model.

Errors

Errors reach your app in three ways:

  • InquiryLifecycle.Failed carries the raw InquiryError when the inquiry cannot continue. An entry with a blank launch code, code or inquiryId fails with InvalidEntry without a request. A launch code that was already used, has expired or is not valid fails with LaunchCodeInvalid, unless this device exchanged that same code before and resumes its inquiry instead. When Folio cannot tell who launched the inquiry, the exchange fails with LaunchInvalid. When the SDK cannot read an inquiry id from what the exchange returns, it fails with NoActiveInquiry. An expired or invalid inquiry token fails with TokenExpired or TokenInvalid. Get a fresh launch code from your backend for these. A mapper that throws, or an image of the verified identity the SDK cannot read, ends the inquiry with RecordMapping; an image larger than 32 MiB ends it with FileTooLarge. Nothing is saved in either case.
  • InquiryUiModel.error is an InquiryErrorView with text, placement and retryable. The ready-made UI shows it at its field or in a banner, and offers a retry only when retryable is true. Your app does not handle it.
  • InquiryUiModel.failure is the notice of an inquiry that cannot go on, with its title, description, an optional retry label and a close label, all chosen by the SDK. A launch code that was already used, has expired or is not valid shows the launch code notice, any other rejected link the invalid link notice, a resume that cannot continue the cannot-resume notice, and any other failure the generic one. The ready-made UI shows it instead of any step on every platform. See Failure notice.

Some errors guard what Folio sends. A step link or terminal redirect whose URL does not parse fails with InvalidLink, whose url is the rejected value. A selfie photo step that names no field for its photo fails with MissingCaptureField, whose step is the step id. A selfie liveness step whose service address is not the Folio selfie service fails with InvalidLink, and a selfie capture that is incomplete or does not fit its state with InvalidState; see Selfie capture. A field without a label, a one-time code length outside 4 to 8, a phone country the SDK cannot offer or a missing default country, and a date field with a date, date format, time zone or weekday the SDK cannot use fail the same way. In every case the SDK does not apply the update that carried them, so the UI never shows that step. Your app cannot fix any of them: the step comes from Folio. See Errors for the cases.

When the device refuses to open a link of the flow, the ready-made UI sends LinkRefused and shows the banner error the store projects. When it refuses the redirect at the end of the inquiry, the UI shows that error with a close button instead of closing the inquiry.

ErrorSwiftKotlinTypeScript type
InvalidEntry.invalidEntryInquiryError.InvalidEntryINVALID_ENTRY
LaunchInvalid.launchInvalidInquiryError.LaunchInvalidLAUNCH_INVALID
LaunchCodeInvalid.launchCodeInvalidInquiryError.LaunchCodeInvalidLAUNCH_CODE_INVALID
TokenExpired.tokenExpiredInquiryError.TokenExpiredTOKEN_EXPIRED
TokenInvalid.tokenInvalidInquiryError.TokenInvalidTOKEN_INVALID
NoActiveInquiry.noActiveInquiryInquiryError.NoActiveInquiryNO_ACTIVE_INQUIRY
RecordMapping.recordMapping(message:)InquiryError.RecordMapping(message)RECORD_MAPPING
FileTooLarge.fileTooLarge(limitBytes:)InquiryError.FileTooLarge(limitBytes)FILE_TOO_LARGE
InvalidLink.invalidLink(url:)InquiryError.InvalidLink(url)INVALID_LINK
MissingCaptureField.missingCaptureField(step:)InquiryError.MissingCaptureField(step)MISSING_CAPTURE_FIELD

The store accepts every action; after Close, every action except Open is a no-op. dispatch throws only when the store is unmounted or the runtime has shut down. The ready-made UI sends nothing after its own Close until an Open of your app starts a new inquiry on the store. On iOS it also drops an action that reaches a store your app has unmounted, and any other throw is a programming error that stops the app. On Android it drops an action while the lifecycle is Closed and once the inquiry UI leaves the composition. On the web it drops an action only after its own Close and once FolioInquiry unmounts, and dispatch on a store your app has closed throws: remove FolioInquiry before you close the store. The embed reports its own failures as FAILED with the InquiryError EMBED or BOOT_FAILED. See Errors for every case.

Platform differences

TopiciOSAndroidWebEmbed
Where the flow runsIn your app, on your runtimeIn your app, on your runtimeIn your page, on your runtimeIn an iframe on Folio's origin, with its own runtime
Who mounts the storeYour appYour app, or FolioInquiry.openYour app, or openInquiryThe iframe
Who closes the storeYour appYour appYour appNobody: the store lives until the iframe is removed, by destroy() or by the modal at CLOSED
End of the flowYour app removes the view when the lifecycle is closedpresent finishes the activity; with Host your app reacts to ClosedYour app reacts to CLOSED in the store stateA modal removes itself at CLOSED; a built-in iframe stays until destroy()
Document photoTaken automatically when the SDK decides; a shutter button appears after five secondsTaken with the shutter buttonTaken automatically when the SDK decides; a capture button appears after five secondsAs on the web
NFC chip readingYesYesNo: the NFC screens are shown, but no chip is readNo
Selfie engineDefaultSelfieCaptureProvider of FolioInquiryUI, which the InquiryPlatform uses by defaultDefaultSelfieCaptureProvider of id.folio:sdk, which the InquiryPlatform uses by defaultBuilt in; a dependency of @folio/sdkBuilt in
Failed liveness runThe flow goes back one stepThe flow goes back one stepA dialog with a retry button that runs the liveness check againAs on the web
System BackNot applicable; how the presented view can be dismissed is up to your appHandled by the flow: closes an open sheet, goes back one step, asks before leaving a first step, otherwise sends Close. See System BackThe browser's Back is your page's navigationThe browser's Back is your page's navigation
Redirect at the endHanded to UIApplication.shared.open, which applies universal-link handling when an app claims the URL, then the flow closes; a refused redirect shows an error with a close buttonOpened with an Intent.ACTION_VIEW intent, then the flow closes; a refused redirect shows an error with a close buttonThe page navigates to it in the same tab, then the flow closes; a refused redirect shows an error with a close buttonThe iframe navigates to it, then the flow closes; your page is not navigated
Camera access deniedThe document camera shows the denied copy with an openSettings button that opens the app's settingsAs on iOS, through the app details settingsThe denied copy, with no settings buttonAs on the web
File field sourceA dialog offers the photo library and Files when constraints.sources is set; otherwise FilesThe system file picker, which lists photosThe browser file pickerAs on the web
Form keyboardReturn moves to the next text field and is Done on the last one; a button press dismisses the keyboardAs on iOSBrowser defaultAs on the web

The camera, NFC, native library and bundler settings the inquiry UI depends on are part of each platform's setup: see iOS, Android and Web. For the embed, see Inquiry embed.

On this page