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:
| Page | Covers |
|---|---|
| Inquiry store | InquiryStore: entries, actions, intents, state, stages and errors in the model |
| Inquiry embed | The script-tag loader for web pages that do not run the SDK |
| NFC chip reading | MrtdStore and the chip reading step |
| Selfie capture | The selfie step and the selfie capture providers |
| Identity records | IdentityRecordMapper and how a verified identity is saved |
| Errors | InquiryError, 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:
| Stage | What the person does |
|---|---|
Screen | Reads text and fills in fields: text, numbers, dates, addresses, phone numbers, countries, selections, checkboxes and files. A screen can carry a language selector. |
Verification | Goes through one verification step. See the next table. |
Handoff | Moves 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. |
Terminal | Reaches the end of the inquiry. |
The verification steps and their screens:
| Step | Screens | What the person does |
|---|---|---|
GovernmentId | Country, Template, Scan, Confirm, UploadReview, Error | Picks the country and the document, takes photos of the document with the camera or picks a file, and confirms them. |
GovernmentIdNfc | Info, Details, Scan, Error | Holds the passport or ID card to the phone so that the chip can be read. See NFC chip reading. |
Selfie | Preparing, Intro, Liveness, Passive, Photo, Review, Error | Takes 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. |
EmailOtp | Bound, Verify, Confirm, Error | Enters an email address, or confirms sending the code to the one the inquiry already holds, then enters the one-time code sent to it. |
PhoneOtp | Bound, Verify, Confirm, Error | Enters 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
- 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.
- Your backend passes the launch code to your app.
- Your app opens the inquiry. It mounts an
InquiryStoreon itsFolioSdkruntime, sendsOpenwith the entryStartand the launch code, and hands the store to the ready-made UI. The SDK exchanges the launch code with Folio for the inquiry. On AndroidFolioInquiry.openand on the webopenInquirymount the store and sendOpenin one call. The ready-made UI itself never sendsOpen. - The inquiry starts. The lifecycle is
Readywhile the store waits for the inquiry, thenStartedwith acorrelationId, the inquiry id. Use it to tie your logs to the inquiry. - The person goes through the steps. The UI renders each stage and sends one intent per interaction. The SDK submits the steps to Folio.
- The verified identity is saved. When a government ID verification succeeds, the SDK calls
your
IdentityRecordMapperwith theVerifiedIdentityand saves theRecordContentit returns in the vault. The identity is also inInquiryUiModel.identity. Selfie and one-time code results never reach the mapper. See Identity records. - The inquiry ends. While the
Terminalstage shows, the lifecycle staysStarted. Once the store has finished loading, the UI opens the stage'sredirect, when there is one, and sendsClose. The redirect is aUniversallink 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 thatClosethe lifecycle becomesClosedwith the outcome. When the person leaves before the end, the UI sendsCloseand the lifecycle becomesClosedwithout 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, andFolioDocumentListStoreshows it; see Folio documents. - 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 ownCloseuntil your app sends a newOpenon 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:
| Entry | Swift | Kotlin | TypeScript |
|---|---|---|---|
| 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
| Platform | Entry points | Package |
|---|---|---|
| iOS | The SwiftUI view FolioInquiry(store:platform:), with an InquiryPlatform(nfc:selfieCapture:). In UIKit, host it in a UIHostingController. | FolioInquiryUI |
| Android | FolioInquiry.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 |
| Web | openInquiry(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.
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:
<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.Failedcarries the rawInquiryErrorwhen the inquiry cannot continue. An entry with a blank launch code, code orinquiryIdfails withInvalidEntrywithout a request. A launch code that was already used, has expired or is not valid fails withLaunchCodeInvalid, unless this device exchanged that same code before and resumes its inquiry instead. When Folio cannot tell who launched the inquiry, the exchange fails withLaunchInvalid. When the SDK cannot read an inquiry id from what the exchange returns, it fails withNoActiveInquiry. An expired or invalid inquiry token fails withTokenExpiredorTokenInvalid. 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 withRecordMapping; an image larger than 32 MiB ends it withFileTooLarge. Nothing is saved in either case.InquiryUiModel.erroris anInquiryErrorViewwithtext,placementandretryable. The ready-made UI shows it at its field or in a banner, and offers a retry only whenretryableistrue. Your app does not handle it.InquiryUiModel.failureis 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.
| Error | Swift | Kotlin | TypeScript type |
|---|---|---|---|
InvalidEntry | .invalidEntry | InquiryError.InvalidEntry | INVALID_ENTRY |
LaunchInvalid | .launchInvalid | InquiryError.LaunchInvalid | LAUNCH_INVALID |
LaunchCodeInvalid | .launchCodeInvalid | InquiryError.LaunchCodeInvalid | LAUNCH_CODE_INVALID |
TokenExpired | .tokenExpired | InquiryError.TokenExpired | TOKEN_EXPIRED |
TokenInvalid | .tokenInvalid | InquiryError.TokenInvalid | TOKEN_INVALID |
NoActiveInquiry | .noActiveInquiry | InquiryError.NoActiveInquiry | NO_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
| Topic | iOS | Android | Web | Embed |
|---|---|---|---|---|
| Where the flow runs | In your app, on your runtime | In your app, on your runtime | In your page, on your runtime | In an iframe on Folio's origin, with its own runtime |
| Who mounts the store | Your app | Your app, or FolioInquiry.open | Your app, or openInquiry | The iframe |
| Who closes the store | Your app | Your app | Your app | Nobody: the store lives until the iframe is removed, by destroy() or by the modal at CLOSED |
| End of the flow | Your app removes the view when the lifecycle is closed | present finishes the activity; with Host your app reacts to Closed | Your app reacts to CLOSED in the store state | A modal removes itself at CLOSED; a built-in iframe stays until destroy() |
| Document photo | Taken automatically when the SDK decides; a shutter button appears after five seconds | Taken with the shutter button | Taken automatically when the SDK decides; a capture button appears after five seconds | As on the web |
| NFC chip reading | Yes | Yes | No: the NFC screens are shown, but no chip is read | No |
| Selfie engine | DefaultSelfieCaptureProvider of FolioInquiryUI, which the InquiryPlatform uses by default | DefaultSelfieCaptureProvider of id.folio:sdk, which the InquiryPlatform uses by default | Built in; a dependency of @folio/sdk | Built in |
| Failed liveness run | The flow goes back one step | The flow goes back one step | A dialog with a retry button that runs the liveness check again | As on the web |
| System Back | Not applicable; how the presented view can be dismissed is up to your app | Handled by the flow: closes an open sheet, goes back one step, asks before leaving a first step, otherwise sends Close. See System Back | The browser's Back is your page's navigation | The browser's Back is your page's navigation |
| Redirect at the end | Handed 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 button | Opened with an Intent.ACTION_VIEW intent, then the flow closes; a refused redirect shows an error with a close button | The page navigates to it in the same tab, then the flow closes; a refused redirect shows an error with a close button | The iframe navigates to it, then the flow closes; your page is not navigated |
| Camera access denied | The document camera shows the denied copy with an openSettings button that opens the app's settings | As on iOS, through the app details settings | The denied copy, with no settings button | As on the web |
| File field source | A dialog offers the photo library and Files when constraints.sources is set; otherwise Files | The system file picker, which lists photos | The browser file picker | As on the web |
| Form keyboard | Return moves to the next text field and is Done on the last one; a button press dismisses the keyboard | As on iOS | Browser default | As 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.