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:

Swift
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:

PlatformEntry point
iOSThe SwiftUI view FolioInquiry(store:platform:)
AndroidFolioInquiry.Host(store, platform) in Compose, FolioInquiry.present(activity, store, platform) in its own activity
WebThe 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.

EntryFieldsMeaning
StartlaunchCodeStart a new inquiry with the one-time launch code your backend created.
Resumesource (ResumeSource)Continue an inquiry that already started.

ResumeSource says where the inquiry continues:

SourceFieldsMeaning
SameDeviceinquiryId, optionalResume on this device. With an inquiryId, that inquiry; without one, the current inquiry this device saved.
AnotherDevicecodeContinue 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.

EntrySwiftKotlinTypeScript
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:

ActionFieldsWhat it does
Openentry (InquiryEntry)Resets the store and starts or resumes the inquiry the entry names.
CloseEnds the inquiry on this device. The lifecycle becomes Closed. Every later action except Open is a no-op.
StatusFetches the inquiry's current state and step from Folio again.
Actthe 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.

ActionSwiftKotlinTypeScript
Open.open(entry: entry)InquiryAction.Open(entry)InquiryAction.open(entry)
Close.closeInquiryAction.CloseInquiryAction.close
Status.statusInquiryAction.StatusInquiryAction.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 send Upload with the bytes. Do not send Capture itself; 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, send LinkRefused with that url.

The cases, with their fields:

IntentFieldsWhat it does
SubmitSubmits the current step with the values the store kept: a screen's fields, or the value of a verification step.
Editfield, 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.
Capturefield, mode (CaptureMode)Tells your UI to capture a file for field. Handled by your UI.
SubmitCapturecaptureSessionId, transactionIdSubmits a selfie liveness run that the capture engine completed, by its transaction id. See Selfie capture.
RetakeTakes the current capture again.
RetryRetries. Without a current step it opens the same entry again; with one, it retries the current step.
ResendSends the one-time code again.
SkipDeclines the current verification step.
CompleteCompletes the inquiry.
CancelCancels the inquiry at Folio and forgets it on this device.
HandoffMoves the inquiry to another device. See Handoff.
ContinueHereTakes control of the inquiry on this device. See Control of the inquiry.
OpenLinklink (LinkTarget)Tells your UI to open a link. Handled by your UI.
LinkRefusedurlReports that the device refused to open a link. The store shows a banner error.
BackGoes back to the previous step.
SetLocalelocaleSwitches the inquiry to another language. See Language.
Uploadfield, bytesUploads the raw bytes of a captured or picked file for field. The SDK encrypts them; you name no file name and no content type.
UploadCapturecaptureSessionId, field, bytesUploads the selfie photo of a Photo state for field and submits it. See Selfie capture.
FileUnreadablefieldReports that a picked file could not be read. The store shows an error at that field.
PreviewFrameluma, width, heightOne 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:

IntentSwiftKotlinTypeScript
Submit.submitInquiryIntent.SubmitInquiryIntent.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.continueHereInquiryIntent.ContinueHereInquiryIntent.continueHere
Retry.retryInquiryIntent.RetryInquiryIntent.retry
Back.backInquiryIntent.BackInquiryIntent.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:

ValueFields
Textvalue: string
Numbervalue: number
Boolvalue: boolean
Datevalue: string
Selectionvalue: string
Listvalues: list of strings
Addressparts: 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's MaskView and shows the masked text. Any other value type fails with Platform.
  • Date fields. A date field without time takes the typed text as Text. The store applies the mask of DateConstraints, shows the masked text and submits the ISO date YYYY-MM-DD. A field with includeTime takes the picked date and time as a Date with an ISO value YYYY-MM-DDTHH:MM. Its value stays empty until the person picks one. The store checks the date and shows a violation in the field's error: 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 Phone field has no value. Its input (PhoneInputView) names two ids: send the chosen country under input.country.field as a Selection with the country of an option in constraints.countryOptions, and the typed number under input.national.field as Text. The store formats the number, builds the full phone number under the field's key, and shows input.country.selected and input.national.text. An Edit under the field's own key, a country outside the options, or another value type fails with Platform.

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.

CaptureMode tells your UI how to capture:

ModeFieldsMeaning
CameraTake a photo with the camera.
Fileaccept, optional maxSizeBytes, extensions, tooLargeError, typeNotAllowedErrorPick 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:

TargetSwiftKotlinTypeScriptHow 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.ID becomes https://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

FieldTypeMeaning
lifecycleInquiryLifecycleWhere the inquiry is: ready, started, closed or failed. See Lifecycle.
viewInquiryView, optionalWhat to show. Empty until an inquiry is open.
loadingbooleantrue while a request to Folio runs.
busybooleantrue while the current verification step is being checked. Show its processing view then.
chromeInquiryChromeViewThe localized texts of the flow around the steps. See Chrome texts.
errorInquiryErrorView, optionalThe current error, projected for the screen. See Errors in the model.
failureInquiryFailureView, optionalThe notice for an inquiry that cannot go on. Show it instead of everything else. See Failure notice.
identityVerifiedIdentity, optionalThe 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:

FieldTexts
errortitle, description, retry and close of the error screen.
clearSelectionThe label that clears a selection.
loadingThe label of the loading state.
nfcThe 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:

FieldMeaning
titleThe title of the notice.
descriptionThe text of the notice.
retryOptional. The label of a retry button. Without it, offer no retry.
closeThe label of the button that leaves the inquiry.

The SDK sets failure in these cases:

CaseTexts
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 stepThe notice that the inquiry cannot be resumed, without retry.
Any other failure that leaves no step to show, once loading is falseThe 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 identityThe 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.

LifecycleFieldsWhen
ReadysdkVersion, optionalThe store is mounted and no inquiry runs yet, including while Open is still loading. sdkVersion is empty in a build without a version stamp.
StartedcorrelationIdThe inquiry runs. correlationId is the inquiry id; use it to tie your logs to the inquiry.
Closedoutcome (TerminalOutcome), optionalYour UI sent Close. outcome is the final status the inquiry had reached, or empty when it reached none.
Failederror (InquiryError)Open failed before the inquiry started, or saving the verified identity failed (RecordMapping or FileTooLarge). The UI shows the failure notice.
LifecycleSwiftKotlinTypeScript 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:

OutcomeSwiftKotlinTypeScriptMeaning
Completed.completedTerminalOutcome.COMPLETED'COMPLETED'The inquiry completed or was approved.
Failed.failedTerminalOutcome.FAILED'FAILED'The inquiry failed or was declined.
Error.errorTerminalOutcome.ERROR'ERROR'The inquiry ended with an error.
Canceled.canceledTerminalOutcome.CANCELED'CANCELED'The inquiry was canceled.
Expired.expiredTerminalOutcome.EXPIRED'EXPIRED'The inquiry expired.
NeedsReview.needsReviewTerminalOutcome.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:

ContentFieldsWhat to show
Screenscreen (ScreenView)A screen of text, images, fields and buttons.
Verificationverification (VerificationView)A verification step, such as a document scan.
Handoffhandoff (HandoffView)The handoff to another device.
Terminaloutcome (TerminalOutcome), optional redirect (LinkTarget)The end of the inquiry.
  • navigation (StageNavigation): canGoBack asks for a back control that sends Back, and canComplete tells that the inquiry can complete. Its optional bar (NavigationBarView) holds the texts of the navigation: backLabel (optional) and closeLabel label the controls, and cancelDialog (CancelDialogView) holds the title, prompt, confirmText and dismissText of the dialog that confirms leaving the inquiry.
  • errors (StepErrorsView): transportError is the generic error text for this step. Show it when the model's error has no text of its own.
  • redirect of Terminal: the link to open when the inquiry ends. It is always a Universal link target, checked and normalized like every other link. Wait until loading is false: the request that completes the inquiry can still be running when the Terminal stage appears. Then open the redirect the way you open every other LinkTarget, and send Close. Without a redirect, send Close at once. When the device refuses to open the redirect, send LinkRefused instead of Close, show the error the store then projects with a close control labeled chrome.error.close, and send Close from 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.

ElementFields
Textid, text, style (TextStyle), optional color (TextColorView)
Imageid, source (ImageRef), optional size (ImageScale)
Spacerid, height
LanguageSelectorid, availableLanguages, currentLocale
Buttona ButtonView
Fielda FieldView
  • TextStyle is Title, Body, Caption or FinePrint. TextColorView is Primary or Secondary.
  • ImageRef is Asset with an assetId, or Preset with an ImagePresetView: one of the illustrations and icons the inquiry UI ships. ImageScale is S, M, L, Xl or Xxl.
  • FieldView is one of Text, Url, TextArea, Number, Masked, Phone, Date, Address, Country, Select, MultiSelect, Radio, Checkbox, CheckboxGroup and File. Every case has id, key, required, readOnly, constraints and an optional error, the text to show under the field. Every case except Phone has an optional value; Phone has input instead. Most cases also have a placeholder and a label, the final label with the required marker; the choice cases have options, and Checkbox has a text. See Field values for how to edit each one.
  • MaskedConstraints.mask and DateConstraints.mask are a MaskView with format, pattern (# stands for a digit), an optional placeholder and maxLength. The mask of a date field follows the field's date format, for example ##.##.#### with the placeholder DD.MM.YYYY. DateConstraints.includeTime asks for a date and time picker instead of the typed date.
  • PhoneConstraints has countryOptions, each a PhoneDialCodeOptionView with country, name, dialCode, label and an optional flag, showCountryCodeSelector and an optional searchPlaceholder. PhoneInputView has country (PhoneCountryView with field and the selected option) and national (PhoneNationalView with field and text).

ButtonView describes a button:

FieldMeaning
id, textThe button id and its label.
styleButtonVariant: Primary, Tonal or Ghost.
enabledWhether the button can be tapped. On a one-time code step, submit stays disabled until the value is complete.
intentThe intent to send when it is tapped.
cooldownSecondsOptional. After a tap, keep the button disabled for this many seconds.
countdownTextOptional. The label during the cooldown; {seconds} in it stands for the seconds left.
sentTextOptional. The label right after a tap, while the cooldown starts.
autoSubmitCountdownTextOptional. 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:

StepState typeStates
GovernmentIdGovIdStateViewCountry, Template, Scan, Confirm, UploadReview, Error
GovernmentIdNfcGovIdNfcStateViewInfo, Details, Scan, Error. See NFC chip reading.
SelfieSelfieStateViewPreparing, Intro, Liveness, Passive, Photo, Review, Error
EmailOtpEmailStateViewBound, Verify, Confirm, Error
PhoneOtpPhoneStateViewBound, 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.

EnumerationSwiftKotlinTypeScript
DocumentSide.front, .back, .barcodeDocumentSide.FRONT, DocumentSide.BACK, DocumentSide.BARCODE'FRONT', 'BACK', 'BARCODE'
NfcDocumentType.passport, .idCardNfcDocumentType.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.

StateSwiftKotlinTypeScript type
Email Bound.bound(title:description:footer:buttons:processing:)EmailStateView.BoundBOUND
Phone Bound.bound(title:description:footer:buttons:processing:)PhoneStateView.BoundBOUND

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 Liveness or Passive, send SubmitCapture with capture.captureSessionId and the engine's transaction id. See Selfie capture.
  • In Photo, captureField names the field the photo is uploaded to. Send the photo with UploadCapture with the state's captureSessionId, that field and the raw bytes, and send FileUnreadable for that field when the photo cannot be read. Upload is not accepted on a selfie step: it fails with InvalidStepType.

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:

  1. Send each camera preview frame as PreviewFrame: the Y plane of the frame, width * height bytes. A frame of any other size sets a Platform error.
  2. Render trigger (CaptureTrigger): Searching, Suggested, AboutToDecide with remainingMillis, or Decided.
  3. Take the photo when shouldCapture is true, and send it with Upload.

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:

FieldMeaning
textOptional. The text to show. When it is empty, show the step's errors.transportError or your generic text.
placementInquiryErrorPlacement: Field with the field it belongs to, or Banner.
retryableWhether 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, contactField or codeField, with the message from Folio.
  • FileUnreadable is placed at its field, with a localized text, and is not retryable.
  • LinkRefused is a banner with the localized text of chrome.error.description, and is not retryable.
  • SchemaValidation and FieldValidation are banners with the message from Folio.
  • FileTooLarge is a banner with a localized text that names the size limit, and is not retryable.
  • Every other error is a banner without text.
  • retryable is true for Network, LocaleNotSaved and an internal error of Folio, and false for any failure of a Resume entry 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:

FieldMeaning
titleThe title of the handoff screen.
descriptionThe description of the handoff screen.
codeThe handoff code.
expiresAtMsWhen the code expires, in milliseconds since the Unix epoch.
channelHandoffChannel: how the code reaches the other device. See the next table.
ChannelFieldsWhat the ready-made UIs show
Qrqr, qrAlt, optional linkqr as a QR code with qrAlt as its text alternative, and link below it.
Linkurl, copyLabelThe url with a copy control labeled copyLabel.
EmailThe code.
SmsThe 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.

ChannelSwiftKotlinTypeScript type
Qr.qr(qr:qrAlt:link:)HandoffChannel.Qr(qr, qrAlt, link)QR
Link.link(url:copyLabel:)HandoffChannel.Link(url, copyLabel)LINK
Email.emailHandoffChannel.EmailEMAIL
Sms.smsHandoffChannel.SmsSMS

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.

On this page