Inquiry store
Drive or observe an identity verification with InquiryStore, its actions, intents, state and lifecycle.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
InquiryStore runs one inquiry: one identity verification of one person. It starts or resumes the
inquiry, talks to Folio, and publishes an InquiryUiModel that is already shaped for the screen.
The ready-made inquiry UI is a view over this store. Use the store directly when
you want to:
- observe an inquiry that the ready-made UI runs, for example to react when it closes or to read the verified identity;
- build your own screens for the inquiry.
The store holds every decision: which request to send, which screen comes next, where an error belongs and whether it can be retried. Your code renders the model and sends one intent per user interaction.
Mount the store and open an inquiry
InquiryStore mounts on any runtime that is an InquiryHost. FolioSdk is one. See
Runtime and stores for the members every store shares.
A new store is idle. Send Open with an entry to start or resume an inquiry:
let inquiry = try InquiryStore.mount(runtime: sdk)
try inquiry.dispatch(action: .open(entry: .start(launchCode: inquiryLaunchCode)))
let lifecycle = inquiry.$state
.map(\.lifecycle)
.sink { lifecycle in
if case let .closed(outcome) = lifecycle {
print("closed:", String(describing: outcome))
}
}InquiryStore.mount works on every platform. On iOS, state is a published property; on Android
it is a StateFlow<InquiryUiModel>; on the web it is a getter, and subscribe takes a listener and
returns the function that removes it.
Every platform also has the parameterless subscribe(), which returns an InquirySubscription. Its
async next() returns the current InquiryUiModel first, then the latest state after each change
(intermediate states can be skipped), and returns no model (nil, null or undefined) once the
store is closed or the runtime has shut down. In Swift the subscription is an AsyncSequence, in
Kotlin asFlow() turns it into a Flow, and on the web next takes an optional { signal } to
abort the wait. On iOS and Android, streamError holds the error that ended the store's own state
stream, if any.
On Android, rememberInquiryStore() mounts the store on the runtime of the enclosing
FolioSDKProvider, or of an InquiryHostProvider, and closes it when it leaves the composition.
Outside Compose, FolioInquiry.open(runtime, entry) does both steps: it mounts an InquiryStore
and dispatches Open with the entry. If the dispatch fails, it closes the store and rethrows.
In React, useInquiryStore() from @folio/sdk/stores mounts the store on the runtime of the
enclosing FolioSDKProvider, or of an InquiryHostProvider, closes it on unmount and returns
{ store, state }. Both are undefined until the store is mounted.
Close the store when you no longer need it. The ready-made UI never closes the store you pass to it.
Show the ready-made UI on your store
The ready-made UI takes the store you mounted, so you can observe the same state it renders:
| Platform | Entry point |
|---|---|
| iOS | The SwiftUI view FolioInquiry(store:platform:) |
| Android | FolioInquiry.Host(store, platform) in Compose, FolioInquiry.present(activity, store, platform) in its own activity |
| Web | The React component FolioInquiry from @folio/sdk/inquiry, with the props store and platform |
The ready-made UI does not open the inquiry: dispatch Open yourself, or use FolioInquiry.open
on Android and openInquiry on the web, which mount the store and dispatch Open with the entry.
See Inquiry UI.
Entries
InquiryEntry names the inquiry that Open starts or resumes.
| Entry | Fields | Meaning |
|---|---|---|
Start | launchCode | Start a new inquiry with the one-time launch code your backend created. |
Resume | source (ResumeSource) | Continue an inquiry that already started. |
ResumeSource says where the inquiry continues:
| Source | Fields | Meaning |
|---|---|---|
SameDevice | inquiryId, optional | Resume on this device. With an inquiryId, that inquiry; without one, the current inquiry this device saved. |
AnotherDevice | code | Continue on this device an inquiry that started on another device, with the handoff code from that device. This device joins as an observer until it takes control. See Handoff and Control of the inquiry. |
The SDK never creates an inquiry. Your backend creates it and passes the launch code to your app.
An entry with a blank launchCode, code or inquiryId fails at once with the error
InvalidEntry, without a request.
Start exchanges the launch code with Folio for the inquiry. A launch code is single-use. The SDK
remembers on this device which inquiry each exchanged launch code opened, so a Start with a code
this device already exchanged resumes that inquiry without a second exchange, until the SDK forgets
the inquiry. Any other code that was already used, has expired or is not valid fails with
LaunchCodeInvalid. When Folio cannot tell who launched the inquiry, the exchange fails with
LaunchInvalid. The SDK reads the inquiry id from what the exchange returns; when it cannot,
Start fails with NoActiveInquiry. It also verifies the signed window attestation that Folio
returns with the exchange; one that does not verify fails with WindowAttestationInvalid.
The SDK saves every inquiry it starts or resumes on this device, and the last one becomes the
current inquiry. A same-device resume fails with NoActiveInquiry when its inquiryId names an
inquiry this device has not saved, or, without an inquiryId, when this device has no current
inquiry. The Cancel and Complete intents forget the inquiry on this device, and so does any
update that finds the inquiry at a final status. When a same-device resume fails with
TokenExpired, TokenInvalid or Expired, the SDK forgets that saved inquiry.
| Entry | Swift | Kotlin | TypeScript |
|---|---|---|---|
| Start | .start(launchCode: launchCode) | InquiryEntry.Start(launchCode) | InquiryEntry.start(launchCode) |
| Resume an inquiry on this device | .resume(source: .sameDevice(inquiryId: id)) | InquiryEntry.Resume(ResumeSource.SameDevice(id)) | InquiryEntry.resume(ResumeSource.sameDevice(id)) |
| Resume the current inquiry | .resume(source: .sameDevice(inquiryId: nil)) | InquiryEntry.Resume(ResumeSource.SameDevice(null)) | InquiryEntry.resume(ResumeSource.sameDevice(undefined)) |
| Resume an inquiry from another device | .resume(source: .anotherDevice(code: code)) | InquiryEntry.Resume(ResumeSource.AnotherDevice(code)) | InquiryEntry.resume(ResumeSource.anotherDevice(code)) |
In TypeScript an entry is a value with a type (START, RESUME) and its fields under value,
and a source has the type SAME_DEVICE or ANOTHER_DEVICE.
Actions
InquiryAction has four cases:
| Action | Fields | What it does |
|---|---|---|
Open | entry (InquiryEntry) | Resets the store and starts or resumes the inquiry the entry names. |
Close | Ends the inquiry on this device. The lifecycle becomes Closed. Every later action except Open is a no-op. | |
Status | Fetches the inquiry's current state and step from Folio again. | |
Act | the intent (InquiryIntent) | Sends one user interaction. See Intents. |
Close does not cancel the inquiry at Folio; the Cancel intent does. Before Open, the store
ignores Status and Act. Until Open has produced an inquiry (while it loads, and after it
failed before the first step), Status and every intent except Retry are no-ops: the failure
notice, its error and the lifecycle stay as they were, and Retry opens the same entry again.
After Close, every action except Open is a no-op: it sends nothing, sets no error and leaves
the lifecycle at Closed with its outcome. The next Open starts a new inquiry on the same store.
Once saving the verified identity has failed (the lifecycle is Failed with RecordMapping or
FileTooLarge), Status and every intent, Retry included, are no-ops: Close leaves the
inquiry, and Open starts a new one. dispatch throws only once the store itself is closed
(unmounted) or the runtime has shut down.
Every inquiry action except Act with a PreviewFrame skips the runtime's action queue: the store
handles them in the order you send them, ahead of queued actions, even when the queue is full. A
PreviewFrame goes through the queue; when the queue is full, the frame is dropped and dispatch
still succeeds.
| Action | Swift | Kotlin | TypeScript |
|---|---|---|---|
| Open | .open(entry: entry) | InquiryAction.Open(entry) | InquiryAction.open(entry) |
| Close | .close | InquiryAction.Close | InquiryAction.close |
| Status | .status | InquiryAction.Status | InquiryAction.status |
| Act | .act(value: intent) | InquiryAction.Act(intent) | InquiryAction.act(intent) |
In TypeScript the type of an action is OPEN, CLOSE, STATUS or ACT. The value of ACT is
the intent itself.
Intents
An intent is one user interaction. Most of them you never build yourself: every button in the model
is a ButtonView whose intent the SDK already filled in. When the person taps it, send that
intent. The store keeps what the person typed: each field change is an Edit, and Submit
carries nothing, because the store submits the values it kept. Two intents need your code first, as
the ready-made UI does:
Capture: open the camera or the file picker it asks for, then sendUploadwith the bytes. Do not sendCaptureitself; the store ignores it.OpenLink: open the link it carries. Do not send it; the store ignores it. When the device refuses to open the url, sendLinkRefusedwith that url.
The cases, with their fields:
| Intent | Fields | What it does |
|---|---|---|
Submit | Submits the current step with the values the store kept: a screen's fields, or the value of a verification step. | |
Edit | field, value (InquiryFieldValue) | Stores one field value while the person types. It sends nothing, except that a complete one-time code submits the step by itself. |
Capture | field, mode (CaptureMode) | Tells your UI to capture a file for field. Handled by your UI. |
SubmitCapture | captureSessionId, transactionId | Submits a selfie liveness run that the capture engine completed, by its transaction id. See Selfie capture. |
Retake | Takes the current capture again. | |
Retry | Retries. Without a current step it opens the same entry again; with one, it retries the current step. | |
Resend | Sends the one-time code again. | |
Skip | Declines the current verification step. | |
Complete | Completes the inquiry. | |
Cancel | Cancels the inquiry at Folio and forgets it on this device. | |
Handoff | Moves the inquiry to another device. See Handoff. | |
ContinueHere | Takes control of the inquiry on this device. See Control of the inquiry. | |
OpenLink | link (LinkTarget) | Tells your UI to open a link. Handled by your UI. |
LinkRefused | url | Reports that the device refused to open a link. The store shows a banner error. |
Back | Goes back to the previous step. | |
SetLocale | locale | Switches the inquiry to another language. See Language. |
Upload | field, bytes | Uploads the raw bytes of a captured or picked file for field. The SDK encrypts them; you name no file name and no content type. |
UploadCapture | captureSessionId, field, bytes | Uploads the selfie photo of a Photo state for field and submits it. See Selfie capture. |
FileUnreadable | field | Reports that a picked file could not be read. The store shows an error at that field. |
PreviewFrame | luma, width, height | One camera preview frame of a document scan, for automatic capture. See Document capture. |
This page names cases as Kotlin does. Swift uses the same names in lowerCamelCase with labeled
fields, and TypeScript has a factory per case in lowerCamelCase and a type string in
SCREAMING_SNAKE_CASE on each value (SUBMIT, OPEN_LINK, HANDOFF). The spellings below are the
ones you build yourself, and ContinueHere, which a button of the model carries:
| Intent | Swift | Kotlin | TypeScript |
|---|---|---|---|
| Submit | .submit | InquiryIntent.Submit | InquiryIntent.submit |
| Edit | .edit(field: field, value: .text(value: text)) | InquiryIntent.Edit(field, InquiryFieldValue.Text(text)) | InquiryIntent.edit(field, InquiryFieldValue.text(text)) |
| Submit a capture | .submitCapture(captureSessionId: session, transactionId: id) | InquiryIntent.SubmitCapture(session, id) | InquiryIntent.submitCapture(session, id) |
| Upload a capture | .uploadCapture(captureSessionId: session, field: field, bytes: bytes) | InquiryIntent.UploadCapture(session, field, bytes) | InquiryIntent.uploadCapture(session, field, bytes) |
| Continue here | .continueHere | InquiryIntent.ContinueHere | InquiryIntent.continueHere |
| Retry | .retry | InquiryIntent.Retry | InquiryIntent.retry |
| Back | .back | InquiryIntent.Back | InquiryIntent.back |
| Set the language | .setLocale(locale: locale) | InquiryIntent.SetLocale(locale) | InquiryIntent.setLocale(locale) |
| Upload | .upload(field: field, bytes: bytes) | InquiryIntent.Upload(field, bytes) | InquiryIntent.upload(field, bytes) |
| Unreadable file | .fileUnreadable(field: field) | InquiryIntent.FileUnreadable(field) | InquiryIntent.fileUnreadable(field) |
| Link refused | .linkRefused(url: url) | InquiryIntent.LinkRefused(url) | InquiryIntent.linkRefused(url) |
| Preview frame | .previewFrame(luma: luma, width: width, height: height) | InquiryIntent.PreviewFrame(luma, width, height) | InquiryIntent.previewFrame(luma, width, height) |
In TypeScript, the type of SubmitCapture, UploadCapture and ContinueHere is
SUBMIT_CAPTURE, UPLOAD_CAPTURE and CONTINUE_HERE.
Bytes are Data in Swift, ByteArray in Kotlin and Uint8Array in TypeScript. The width and
height of a preview frame are UInt32 in Swift, Long in Kotlin and number in TypeScript.
Every intent that reaches Folio sets loading and clears the current error until the answer
arrives.
Field values
Edit carries an InquiryFieldValue:
| Value | Fields |
|---|---|
Text | value: string |
Number | value: number |
Bool | value: boolean |
Date | value: string |
Selection | value: string |
List | values: list of strings |
Address | parts: list of AddressPartValue |
AddressPartValue is an AddressPart with its value. Send an Edit under the key of the
FieldView, except for a phone field. A verification step names the field it edits:
selectionField for a document choice, contactField for an email address and codeField for a
one-time code.
The store shapes what it keeps, and the FieldView shows the result in value:
- Masked fields. Send the typed text as
Text. The store applies the field'sMaskViewand shows the masked text. Any other value type fails withPlatform. - Date fields. A date field without time takes the typed text as
Text. The store applies themaskofDateConstraints, shows the masked text and submits the ISO dateYYYY-MM-DD. A field withincludeTimetakes the picked date and time as aDatewith an ISO valueYYYY-MM-DDTHH:MM. Itsvaluestays empty until the person picks one. The store checks the date and shows a violation in the field'serror: a date that does not exist, a date outside the allowed range, a past or future date the field does not allow, an age outside the allowed years, or a disabled weekday. Today is the date in the field's time zone, or in the device's zone when the field names none. - Phone fields. A
Phonefield has novalue. Itsinput(PhoneInputView) names two ids: send the chosen country underinput.country.fieldas aSelectionwith thecountryof an option inconstraints.countryOptions, and the typed number underinput.national.fieldasText. The store formats the number, builds the full phone number under the field'skey, and showsinput.country.selectedandinput.national.text. AnEditunder the field's ownkey, a country outside the options, or another value type fails withPlatform.
Submit then sends every field with the value the store kept, or with its default. A phone field
that the person did not touch sends the number its input shows.
Capture modes and links
CaptureMode tells your UI how to capture:
| Mode | Fields | Meaning |
|---|---|---|
Camera | Take a photo with the camera. | |
File | accept, optional maxSizeBytes, extensions, tooLargeError, typeNotAllowedError | Pick a file. accept and extensions say which files are allowed; the two texts are the errors to show when the file is too large or of the wrong type. |
LinkTarget says how to open a link. Each case has a url:
| Target | Swift | Kotlin | TypeScript | How the ready-made UIs open it |
|---|---|---|---|---|
External | .external(url: url) | LinkTarget.External(url) | LinkTarget.external(url) | The web UI in a new tab. iOS and Android hand it to the OS. |
Universal | .universal(url: url) | LinkTarget.Universal(url) | LinkTarget.universal(url) | The web UI in the same tab. iOS and Android hand it to the OS. |
Webview | .webview(url: url) | LinkTarget.Webview(url) | LinkTarget.webview(url) | The web UI in the same tab. iOS and Android hand it to the OS. |
On iOS and Android the OS applies its own link handling, so a Universal link reaches the app that
handles it as a universal link (iOS) or app link (Android).
When the device refuses to open a valid url, send LinkRefused with that url. The store shows a
Banner error with the localized text of chrome.error.description. Keep no refusal flag of your
own.
The SDK checks and normalizes every link url of a step, and the url of the terminal redirect, before it stores the update:
- A url that does not parse fails the update with
InvalidLink, and the previous state stays. - A url that parses is replaced by its normalized form. For example,
HTTPS://Folio.IDbecomeshttps://folio.id/, and a space becomes%20.
Every url your UI receives therefore parses with URL(string:) on iOS, Uri.parse on Android and
new URL on the web.
State: InquiryUiModel
| Field | Type | Meaning |
|---|---|---|
lifecycle | InquiryLifecycle | Where the inquiry is: ready, started, closed or failed. See Lifecycle. |
view | InquiryView, optional | What to show. Empty until an inquiry is open. |
loading | boolean | true while a request to Folio runs. |
busy | boolean | true while the current verification step is being checked. Show its processing view then. |
chrome | InquiryChromeView | The localized texts of the flow around the steps. See Chrome texts. |
error | InquiryErrorView, optional | The current error, projected for the screen. See Errors in the model. |
failure | InquiryFailureView, optional | The notice for an inquiry that cannot go on. Show it instead of everything else. See Failure notice. |
identity | VerifiedIdentity, optional | The verified identity, once a verification produced one. See Verified identity. |
InquiryView has the inquiry id and an optional stage: the step to show.
Chrome texts
InquiryChromeView holds the texts your UI needs outside a step, in the inquiry's language:
| Field | Texts |
|---|---|
error | title, description, retry and close of the error screen. |
clearSelection | The label that clears a selection. |
loading | The label of the loading state. |
nfc | The states of the chip reader, from holdNearDocument to connectionLost. |
Failure notice
InquiryUiModel.failure is set when the inquiry cannot go on. It is an InquiryFailureView with the
texts already chosen and localized by the SDK:
| Field | Meaning |
|---|---|
title | The title of the notice. |
description | The text of the notice. |
retry | Optional. The label of a retry button. Without it, offer no retry. |
close | The label of the button that leaves the inquiry. |
The SDK sets failure in these cases:
| Case | Texts |
|---|---|
A Start entry fails before its first step because Folio does not accept the launch code, which was already used, has expired or is not valid (LaunchCodeInvalid) | The launch code notice, without retry. In English its title is "This link can’t be used" and its description "This verification link has already been used, has expired or is not valid. Request a new link and try again." |
A Start entry fails before its first step because the link was rejected for another reason (InvalidEntry, LaunchInvalid, TokenInvalid, TokenExpired, NotFound, Expired) | The invalid link notice, without retry. |
A Resume entry, on this device or from another device, fails before its first step | The notice that the inquiry cannot be resumed, without retry. |
Any other failure that leaves no step to show, once loading is false | The generic error notice. The description is the error's text; retry is set exactly when the error is retryable. |
Saving the verified identity failed (RecordMapping or FileTooLarge); see Verified identity | The generic error notice, without retry, even though a step may still be in the model. For FileTooLarge the description names the size limit. |
When failure is set, show it instead of the stage, the loading state and any banner. Show title
and description, a button labeled retry that sends Act(Retry) when retry is set, and always
a button labeled close that sends Close. Do not choose texts by error, entry or lifecycle: the
SDK already did.
Lifecycle
InquiryLifecycle replaces event callbacks. Watch it to know when the inquiry starts and ends.
| Lifecycle | Fields | When |
|---|---|---|
Ready | sdkVersion, optional | The store is mounted and no inquiry runs yet, including while Open is still loading. sdkVersion is empty in a build without a version stamp. |
Started | correlationId | The inquiry runs. correlationId is the inquiry id; use it to tie your logs to the inquiry. |
Closed | outcome (TerminalOutcome), optional | Your UI sent Close. outcome is the final status the inquiry had reached, or empty when it reached none. |
Failed | error (InquiryError) | Open failed before the inquiry started, or saving the verified identity failed (RecordMapping or FileTooLarge). The UI shows the failure notice. |
| Lifecycle | Swift | Kotlin | TypeScript type |
|---|---|---|---|
| Ready | .ready(sdkVersion:) | InquiryLifecycle.Ready(sdkVersion) | READY |
| Started | .started(correlationId:) | InquiryLifecycle.Started(correlationId) | STARTED |
| Closed | .closed(outcome:) | InquiryLifecycle.Closed(outcome) | CLOSED |
| Failed | .failed(error:) | InquiryLifecycle.Failed(error) | FAILED |
In TypeScript the fields are under value, for example lifecycle.value.correlationId.
Only Close ends the lifecycle. An inquiry that reaches a final status stays Started while its
Terminal stage is shown, so the UI can still open the redirect and report a refused one; the UI
then sends Close, and the lifecycle becomes Closed with the outcome. Failed is informational:
the person leaves the failure notice with its close button, which sends Close. Remove your UI and
close the store when the lifecycle is Closed, not when it is Failed. Send nothing after your own
Close: once you close the store, dispatch throws.
InquiryLifecycle.Failed carries the raw InquiryError. It is the
only place the raw error reaches your code; everywhere else the model carries its projection.
Outcomes
TerminalOutcome is the final result of an inquiry:
| Outcome | Swift | Kotlin | TypeScript | Meaning |
|---|---|---|---|---|
Completed | .completed | TerminalOutcome.COMPLETED | 'COMPLETED' | The inquiry completed or was approved. |
Failed | .failed | TerminalOutcome.FAILED | 'FAILED' | The inquiry failed or was declined. |
Error | .error | TerminalOutcome.ERROR | 'ERROR' | The inquiry ended with an error. |
Canceled | .canceled | TerminalOutcome.CANCELED | 'CANCELED' | The inquiry was canceled. |
Expired | .expired | TerminalOutcome.EXPIRED | 'EXPIRED' | The inquiry expired. |
NeedsReview | .needsReview | TerminalOutcome.NEEDS_REVIEW | 'NEEDS_REVIEW' | The inquiry waits for a manual review. |
TerminalOutcome is a plain enumeration: an enum with a UInt8 raw value in Swift, an
enum class in Kotlin and a string in TypeScript, with the constants TerminalOutcome.COMPLETED
and so on.
Stages
InquiryView.stage is an InquiryStage with an optional navigation, optional errors and its
content. The content is one of four cases:
| Content | Fields | What to show |
|---|---|---|
Screen | screen (ScreenView) | A screen of text, images, fields and buttons. |
Verification | verification (VerificationView) | A verification step, such as a document scan. |
Handoff | handoff (HandoffView) | The handoff to another device. |
Terminal | outcome (TerminalOutcome), optional redirect (LinkTarget) | The end of the inquiry. |
navigation(StageNavigation):canGoBackasks for a back control that sendsBack, andcanCompletetells that the inquiry can complete. Its optionalbar(NavigationBarView) holds the texts of the navigation:backLabel(optional) andcloseLabellabel the controls, andcancelDialog(CancelDialogView) holds thetitle,prompt,confirmTextanddismissTextof the dialog that confirms leaving the inquiry.errors(StepErrorsView):transportErroris the generic error text for this step. Show it when the model's error has no text of its own.redirectofTerminal: the link to open when the inquiry ends. It is always aUniversallink target, checked and normalized like every other link. Wait untilloadingisfalse: the request that completes the inquiry can still be running when theTerminalstage appears. Then open the redirect the way you open every otherLinkTarget, and sendClose. Without aredirect, sendCloseat once. When the device refuses to open the redirect, sendLinkRefusedinstead ofClose, show the error the store then projects with a close control labeledchrome.error.close, and sendClosefrom that control. The ready-made UIs do this: the web UI navigates the same tab to the redirect, and on iOS and Android the OS opens it with its universal-link handling.
An inquiry that issues a document does not show the document. It ends like any other inquiry, and
the model carries nothing about the issued document. The SDK receives the issued document into the
vault on its own, and FolioDocumentListStore shows it; see Folio documents.
The inquiry's last screen can offer the document with a button whose intent is OpenLink.
Screens
A ScreenView has the stepId and a list of containers. Each ScreenContainer has an id, a
layout and a list of elements. ContainerLayout is Header, Content, Action, Footer, or
Custom with gap, paddingTop and paddingBottom. Lay containers out by their layout only.
| Element | Fields |
|---|---|
Text | id, text, style (TextStyle), optional color (TextColorView) |
Image | id, source (ImageRef), optional size (ImageScale) |
Spacer | id, height |
LanguageSelector | id, availableLanguages, currentLocale |
Button | a ButtonView |
Field | a FieldView |
TextStyleisTitle,Body,CaptionorFinePrint.TextColorViewisPrimaryorSecondary.ImageRefisAssetwith anassetId, orPresetwith anImagePresetView: one of the illustrations and icons the inquiry UI ships.ImageScaleisS,M,L,XlorXxl.FieldViewis one ofText,Url,TextArea,Number,Masked,Phone,Date,Address,Country,Select,MultiSelect,Radio,Checkbox,CheckboxGroupandFile. Every case hasid,key,required,readOnly,constraintsand an optionalerror, the text to show under the field. Every case exceptPhonehas an optionalvalue;Phonehasinputinstead. Most cases also have aplaceholderand alabel, the final label with the required marker; the choice cases haveoptions, andCheckboxhas atext. See Field values for how to edit each one.MaskedConstraints.maskandDateConstraints.maskare aMaskViewwithformat,pattern(#stands for a digit), an optionalplaceholderandmaxLength. The mask of a date field follows the field's date format, for example##.##.####with the placeholderDD.MM.YYYY.DateConstraints.includeTimeasks for a date and time picker instead of the typed date.PhoneConstraintshascountryOptions, each aPhoneDialCodeOptionViewwithcountry,name,dialCode,labeland an optionalflag,showCountryCodeSelectorand an optionalsearchPlaceholder.PhoneInputViewhascountry(PhoneCountryViewwithfieldand theselectedoption) andnational(PhoneNationalViewwithfieldandtext).
ButtonView describes a button:
| Field | Meaning |
|---|---|
id, text | The button id and its label. |
style | ButtonVariant: Primary, Tonal or Ghost. |
enabled | Whether the button can be tapped. On a one-time code step, submit stays disabled until the value is complete. |
intent | The intent to send when it is tapped. |
cooldownSeconds | Optional. After a tap, keep the button disabled for this many seconds. |
countdownText | Optional. The label during the cooldown; {seconds} in it stands for the seconds left. |
sentText | Optional. The label right after a tap, while the cooldown starts. |
autoSubmitCountdownText | Optional. When present, show it as the label; the ready-made UIs tap the button by themselves after a second. |
Verification steps
VerificationView is one of five steps. Each has a stepId, an attempt (AttemptState) and a
state with the screen of that step:
| Step | State type | States |
|---|---|---|
GovernmentId | GovIdStateView | Country, Template, Scan, Confirm, UploadReview, Error |
GovernmentIdNfc | GovIdNfcStateView | Info, Details, Scan, Error. See NFC chip reading. |
Selfie | SelfieStateView | Preparing, Intro, Liveness, Passive, Photo, Review, Error |
EmailOtp | EmailStateView | Bound, Verify, Confirm, Error |
PhoneOtp | PhoneStateView | Bound, Verify, Confirm, Error |
AttemptState has phase, used, max and canRetry. AttemptPhase is Created, Processing,
Passed, Failed, Error, Canceled or Expired. Most states carry their own title,
description, buttons and an optional processing (ProcessingView with title and
description) to show while the step is being checked. Every Error state is a
VerificationFailureView with a title, its buttons, and an optional description,
failureReason, illustration and processing.
The Scan, Confirm and UploadReview states of GovernmentId carry the side of the document
they show, a DocumentSide: Front, Back or Barcode. The Scan state of GovernmentIdNfc
carries the documentType of the chip to read, an NfcDocumentType: Passport or IdCard. Both
are enumerations; match every case.
| Enumeration | Swift | Kotlin | TypeScript |
|---|---|---|---|
DocumentSide | .front, .back, .barcode | DocumentSide.FRONT, DocumentSide.BACK, DocumentSide.BARCODE | 'FRONT', 'BACK', 'BARCODE' |
NfcDocumentType | .passport, .idCard | NfcDocumentType.PASSPORT, NfcDocumentType.ID_CARD | 'PASSPORT', 'ID_CARD' |
The one-time code steps name the field they edit and the field their errors are placed at:
contactField in Verify and codeField in Confirm. The email Verify state takes the address
as an Edit under contactField. The phone Verify state has input (PhoneInputView) and
countryOptions like a phone field: send the country and the number under the two ids of input,
as described in Field values. The store starts it on the country Folio sets and
builds the phone number under contactField. Confirm has code (OtpCodeView with length,
groups, value and formatted), confirmLimit and codeTtl.
The Bound state replaces Verify when the inquiry already holds the email address or phone
number to verify, so the person enters none. It has title, description, footer, buttons
and an optional processing, and no field. Its submit button carries Submit, which asks Folio
to send the code to that address or number.
| State | Swift | Kotlin | TypeScript type |
|---|---|---|---|
Email Bound | .bound(title:description:footer:buttons:processing:) | EmailStateView.Bound | BOUND |
Phone Bound | .bound(title:description:footer:buttons:processing:) | PhoneStateView.Bound | BOUND |
Selfie capture
A selfie step shows the Preparing state while its capture is being prepared. It carries only
processing, a ProcessingView that is always set: show it, and send nothing. The step moves on
to Liveness, Passive or Photo without an intent from your UI. The state is
.preparing(processing:) in Swift, SelfieStateView.Preparing(processing) in Kotlin and has the
type PREPARING in TypeScript.
Each capture state names its capture session in captureSessionId: on capture
(SelfieCaptureView) in Liveness and Passive, and on the state itself in Photo. A new id
means a new capture session; the ready-made UIs start the camera or the engine again when the id
changes. Send the id back with the result:
- After a liveness run in
LivenessorPassive, sendSubmitCapturewithcapture.captureSessionIdand the engine's transaction id. See Selfie capture. - In
Photo,captureFieldnames the field the photo is uploaded to. Send the photo withUploadCapturewith the state'scaptureSessionId, that field and the raw bytes, and sendFileUnreadablefor that field when the photo cannot be read.Uploadis not accepted on a selfie step: it fails withInvalidStepType.
The store checks the result against the step it holds. A captureSessionId that is not the one of
the current capture, or a blank transaction id, fails with InvalidState; a SubmitCapture outside
Liveness and Passive, or an UploadCapture outside Photo, fails with InvalidStepType; an
UploadCapture for a field the step does not upload to fails with Upload. A selfie Photo step
that Folio sends without a capture field fails the update with the error MissingCaptureField, a
banner without text and without retry, and the previous state stays. The store also checks the
capture of every Liveness, Passive and Photo state: a liveness service address that is not
the Folio selfie proxy of the SDK fails the update with InvalidLink, any other incomplete or
mismatched capture with InvalidState, and the previous state stays. See
The SDK checks each capture.
Document capture
In the Scan state of GovernmentId, camera is an IdCameraScreenView. The SDK decides when to
take the photo:
- Send each camera preview frame as
PreviewFrame: the Y plane of the frame,width * heightbytes. A frame of any other size sets aPlatformerror. - Render
trigger(CaptureTrigger):Searching,Suggested,AboutToDecidewithremainingMillis, orDecided. - Take the photo when
shouldCaptureistrue, and send it withUpload.
The SDK ignores frames once it decided, and while no document scan is active. A frame that arrives while the action queue is full is dropped. The ready-made Android UI sends no frames: it takes the photo with a shutter button.
Errors in the model
InquiryUiModel.error is an InquiryErrorView. It tells you what to show and where, so your UI
keeps no rules of its own:
| Field | Meaning |
|---|---|
text | Optional. The text to show. When it is empty, show the step's errors.transportError or your generic text. |
placement | InquiryErrorPlacement: Field with the field it belongs to, or Banner. |
retryable | Whether to offer a retry. A retry sends Act(Retry). |
How the store places errors:
- A one-time code error the person can fix (
InvalidCode,ExpiredCode,AttemptsExhausted,ConfirmExhausted,InvalidTarget,MissingTarget,DeliveryFailed) is placed at the field of the code step,contactFieldorcodeField, with the message from Folio. FileUnreadableis placed at its field, with a localized text, and is not retryable.LinkRefusedis a banner with the localized text ofchrome.error.description, and is not retryable.SchemaValidationandFieldValidationare banners with the message from Folio.FileTooLargeis a banner with a localized text that names the size limit, and is not retryable.- Every other error is a banner without text.
retryableistrueforNetwork,LocaleNotSavedand an internal error of Folio, andfalsefor any failure of aResumeentry before its first step.
The store also copies the text of a Field error into the error of the matching FieldView, and
editing that field clears it; an edit under input.country.field or input.national.field of a
phone clears the error of that phone. Show a Field error only at its field, and a Banner error
only in the banner.
When there is no stage to show and the error is not retryable, show a final error screen that lets
the person leave. Do not send Open again to retry.
See Errors for every InquiryError case.
Language
A LanguageSelector element lists the languages of the inquiry. Each Language has a code, a
name and a nativeName; currentLocale is the language shown now.
To switch, send SetLocale with the chosen code. The SDK first saves the choice as the SDK's
language setting, then asks Folio for the step in that language, and every text of the model
changes. If the choice cannot be saved, the intent fails with LocaleNotSaved, a retryable banner,
and nothing is sent to Folio. See Localization.
Handoff
The Handoff intent moves the inquiry to another device. The stage becomes Handoff with a
HandoffView:
| Field | Meaning |
|---|---|
title | The title of the handoff screen. |
description | The description of the handoff screen. |
code | The handoff code. |
expiresAtMs | When the code expires, in milliseconds since the Unix epoch. |
channel | HandoffChannel: how the code reaches the other device. See the next table. |
| Channel | Fields | What the ready-made UIs show |
|---|---|---|
Qr | qr, qrAlt, optional link | qr as a QR code with qrAlt as its text alternative, and link below it. |
Link | url, copyLabel | The url with a copy control labeled copyLabel. |
Email | The code. | |
Sms | The code. |
The handoff the Handoff intent requests is Qr. When qr is an http or https URL, link is
an External link target with that URL; otherwise it is empty.
| Channel | Swift | Kotlin | TypeScript type |
|---|---|---|---|
| Qr | .qr(qr:qrAlt:link:) | HandoffChannel.Qr(qr, qrAlt, link) | QR |
| Link | .link(url:copyLabel:) | HandoffChannel.Link(url, copyLabel) | LINK |
.email | HandoffChannel.Email | EMAIL | |
| Sms | .sms | HandoffChannel.Sms | SMS |
expiresAtMs is Int64 in Swift, Long in Kotlin and bigint in TypeScript.
The other device opens the inquiry with the entry Resume and the source AnotherDevice with the
code. The errors of a handoff are HandoffExpired, HandoffConsumed and HandoffMismatch.
Control of the inquiry
One device at a time controls an inquiry, and only that device can move it forward. A device that
opens the inquiry with a handoff code joins it as an observer; the device that handed it off keeps
control until the other device takes it. A device that another device took control from becomes an
observer too, also after a Resume on that device.
On an observer, the stage is a Screen from Folio with a title, a description and a button whose
intent is ContinueHere. Send that intent when the person taps the button, as with every other
button. The store asks Folio to make this device the controller and then shows the current step.
The device that had control becomes an observer and shows the observer screen after its next
request to Folio. Either device can take control back with ContinueHere.
When Folio rejects a request of this device because another device controls the inquiry, the
store marks this device as an observer and reads the inquiry's state again, so the model shows the
observer screen instead of the error. The error of that rejection is NotController; see
Errors. Two requests are the exception: when Folio rejects the request
of ContinueHere that takes control, or the read of the result once the inquiry reaches its end,
the store does not turn the device into an observer, and NotController reaches the model as a
banner, like any other error.
Verified identity
When a government ID verification of the inquiry produces a verified result,
InquiryUiModel.identity holds it as a VerifiedIdentity, the same value your mapper receives:
the inquiry id, the selected document type, the verifier output, the issue and expiry times, and
the stored files. VerifiedIdentity lists its fields and
their types on each platform.
The SDK sets identity only after it has saved the result through your IdentityRecordMapper,
which decides how it is saved in the vault.
Before it calls the mapper, the SDK reads every image of the result. An image larger than 32 MiB
fails with FileTooLarge, whose limitBytes is that limit, and an image it cannot read fails with
RecordMapping, like a mapper that fails. Each of them ends the inquiry: the lifecycle becomes
Failed, nothing is saved, and the model shows the failure notice.