Runtime and stores

Create and close the FolioSdk runtime, mount stores, read state and send actions.

Preview

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

Every app that uses the Folio SDK works through two kinds of objects: one runtime, FolioSdk, and the stores mounted on it. This page describes both and the contract every store shares.

The runtime orchestrates, stores own data access

FolioSdk creates the session, the network transport, the vault and the store loop. It returns no data. Everything your app reads or changes goes through a store:

  • To read something, mount the store that owns it and read its state.
  • To change something, or to load more data, send an action to the store. The result arrives as a new state of the same store.

There is no call on the runtime that fetches data, and no call on a store that returns a result directly. A store's state is the only source your UI renders from.

FolioSdk has these members:

MemberWhat it does
create(config, mapper)Static. Builds the runtime and restores or starts the session. Asynchronous.
localizeCurrent(key)Returns the text for a TextKey in the current SDK locale. See Localization.
shutdown()Unmounts every store, ends the session scope and stops the runtime. Asynchronous.
close()Runs shutdown() once and releases the runtime. Asynchronous; later calls wait for the same close.

How each platform spells them:

MemberSwiftKotlinTypeScript
Createtry await FolioSdk.create(config:mapper:)FolioSdk.create(config, mapper), a suspendawait FolioSdk.create(config, mapper, options)
Localizetry sdk.localizeCurrent(key:)sdk.localizeCurrent(key)sdk.localizeCurrent(key)
Shut downtry await sdk.shutdown()sdk.shutdown(), a suspendawait sdk.shutdown({ signal })
Closetry await sdk.close()sdk.close(), a suspendawait sdk.close(), also [Symbol.asyncDispose]()
Static functionsstatic func on FolioSdk@JvmStatic members of the companion objectstatic methods of FolioSdk

On Android, create is a suspend member of the companion object and, unlike the static functions, carries no @JvmStatic.

Besides these, FolioSdk is the host every store mounts on: it implements SessionHost, AuthHost, VaultHost, InquiryHost, MrtdHost, DiagnosticsHost, FolioDocumentListHost, FolioDocumentHost and Localizing. Code that only needs part of the runtime takes it through these interfaces. The inquiry UI, for example, runs on a mounted InquiryStore; on iOS and Android its InquiryPlatform takes the runtime as the MrtdHost that reads the NFC chip. See Inquiry UI.

Create the runtime

FolioSdk.create takes a FolioSdkConfig and your IdentityRecordMapper. It completes only when the runtime is ready: it initializes the platform services, loads the stored locale, and restores the stored session or starts a new one with the Folio backend. A runtime you receive from create is ready to use; there is no separate start step and no need to wait before mounting stores.

Create one runtime for your app and keep it for as long as you use the SDK. The runtime keeps the mapper for its whole lifetime.

create also sets the host of the flag URLs that the static country helpers return, from baseUrl and for the whole process; see Countries and phone numbers.

Swift
import FolioSDK

let sdk = try await FolioSdk.create(config: config, mapper: SavedIdentityMapper())

On Android, create is a suspend function: call it from a coroutine. Cancelling that coroutine cancels the creation. On the web, create takes an optional third argument, { signal }, with an AbortSignal that cancels the creation; the promise then rejects with a DOMException named AbortError.

Android

Call XPlatform.init(context) once per process before you create a runtime. XPlatform is in the package id.folio.sdk.platform. It loads the SDK's native library; a second call throws "XPlatform already initialized", so call it from your Application.onCreate. XPlatform.isInitialized tells whether it already ran. See Android setup.

In Compose, rememberFolioSdk(config, mapper) creates the runtime for you. It is scoped to the ViewModelStoreOwner of the composition and returns a FolioSdkStartup:

FolioSdkStartupMeaning
Startingcreate is still running.
ReadyThe runtime is ready. runtime holds the FolioSdk.
Failedcreate failed. error holds the Throwable it failed with. retry() runs create again.

rememberFolioSdk closes the runtime when its ViewModel is cleared. When you pass a different config (compared by value) or a different mapper (compared by identity), it closes the current runtime and creates a new one once the old one has finished closing.

MainActivity.kt
setContent {
    when (val startup = rememberFolioSdk(config, SavedIdentityMapper)) {
        FolioSdkStartup.Starting -> Unit
        is FolioSdkStartup.Failed -> Button(onClick = { startup.retry() }) { Text("Retry") }
        is FolioSdkStartup.Ready -> FolioSDKProvider(startup.runtime) { Inquiry() }
    }
}

Web

The web package runs the SDK as WebAssembly. Load it once per page with initializeFolioWasm before you create the runtime. source identifies the WASM you load, application is your web application's identity (see Application identity) and load returns the bytes; loadFolioSDKWasm loads them from the manifest that ships with the package, whose file name is FolioSDKWasmArtifact.manifestFilename. Web setup describes where to serve the files.

main.ts
import { FolioSdk, FolioSDKWasmArtifact, initializeFolioWasm, loadFolioSDKWasm } from '@folio/sdk';

const manifestUrl = `/${FolioSDKWasmArtifact.manifestFilename}`;
await initializeFolioWasm({
    source: manifestUrl,
    application: { identifier: window.location.origin, version: '1.0.0', build: '1' },
    load: () => loadFolioSDKWasm({ manifestUrl }),
});

const sdk = await FolioSdk.create(config, savedIdentityMapper);

initializeFolioWasm loads the WASM only once per page:

  • A second call with the same source returns the same promise.
  • A call with a different source rejects with "Folio WASM is already loaded from ...".
  • If loading fails, the next call tries again.
  • An application field that is missing or blank rejects with an Error that names the field, such as xplatform: application.version must be a non-empty string.

A FolioSdk.create call before the WASM is loaded rejects with the Error "WASM not initialized".

Providers

Each platform has a provider that hands one runtime to the views below it, and helpers that mount a store for the lifetime of a view.

On iOS, FolioSDKProvider(runtime:content:) is a SwiftUI view that puts the runtime into the environment. FolioSdk is an ObservableObject, so a view below reads it with @EnvironmentObject.

Swift
FolioSDKProvider(runtime: sdk) {
    RecordsView()
}

struct RecordsView: View {
    @EnvironmentObject var sdk: FolioSdk

    var body: some View {
        RecordList(runtime: sdk)
    }
}

Close the runtime

shutdown() ends the runtime:

  1. Every mounted store is unmounted. Every open subscription of a store ends: its next() returns no value.
  2. The session scope is shut down.
  3. The store loop stops and the log journal is flushed. shutdown() returns when both have finished, and fails with a CoreError when the store loop ended with an error or the journal could not be flushed (LogsUnavailable).

close() runs shutdown() once and then releases the runtime, also when shutdown() failed. Every call of close() waits for that same close and ends with its result. Close the runtime when your app no longer needs it, and close your own stores before that. After close(), every call on the runtime throws.

Swift
try await sdk.close()

Start failures

create fails when the runtime cannot start. Nothing is created in that case; fix the cause and call create again. The call throws the platform's SDK error: FolioSDKError on iOS, FolioSDKException on Android, and a promise rejected with FolioSDKError on the web. Its message is the text of a SessionStartError:

CaseFieldsWhenMessage starts with
Backendstatus (HTTP status), code, messageThe Folio backend rejected the session start or refresh. code and message come from it.backend rejected authentication
NetworkmessageA request got no HTTP response, for example when the device is offline.backend unreachable
ScopemessageThe services of the session's identity could not be started.session scope is unavailable
Coreerror (CoreError)A local failure: invalid configuration, missing platform services or unavailable storage.the text of the CoreError

The same SessionStartError is available as a value in SessionUiModel.error; see Session for its spelling on each platform.

The most common Core cases from create:

CoreErrorCause
InvalidInputorganization or an entry of serviceEnvironments in the configuration is not valid, or your application's version or build cannot be sent in a header. See Configuration.
NotInitializedThe platform services are not available, for example because XPlatform.init did not run on Android or the platform does not report your application's identifier, version or build.
StorageUnavailableThe device storage could not be read or written.
LogsUnavailableThe log journal configured in FolioSdkConfig.logging could not be started, or could not be stopped after create failed for another reason; then it replaces that error. See Logging.

On Android, rememberFolioSdk reports the failure as FolioSdkStartup.Failed. See Errors for every error type of the SDK.

Stores

A store exists only while it is mounted. You mount it on the runtime, read its state, send it actions, and close it when the screen or feature that uses it goes away. You can mount as many stores as you need, including several instances of the same store.

StoreMount onStateAction
SessionStoreSessionHostSessionUiModelSessionAction
AuthStoreAuthHostAuthUiModelAuthAction
VaultStoreVaultHostVaultUiModelVaultAction
InquiryStoreInquiryHostInquiryUiModelInquiryAction
MrtdStoreMrtdHostMrtdUiModelMrtdAction
LogsStoreDiagnosticsHostLogsUiModelLogsAction
FolioDocumentListStoreFolioDocumentListHostFolioDocumentListUiModelFolioDocumentListAction
FolioDocumentStoreFolioDocumentHostFolioDocumentUiModelFolioDocumentAction

FolioSdk implements every host in the table, so you mount each store on your FolioSdk. Two stores take a mount parameter: LogsStore the LogFilter of the journal it reads, LogsStore.mount(runtime, init), and FolioDocumentStore the FolioDocumentInit with the id of its document, FolioDocumentStore.mount(runtime, init). See Folio documents.

Two more objects mount on DiagnosticsHost but are not stores: they have no state and no actions. InteractionRecorder.mount(runtime) records the user's interactions in the SDK's log journal, and SdkLogger.mount(runtime, target) writes your own messages to it. See Logging.

Store members

Every store has the same members:

MemberWhat it does
mount(runtime)Static. Mounts a new store instance on the runtime and reads its first state. Throws on failure.
stateThe current state. Never empty while the store is mounted.
dispatch(action)Queues one action. Returns as soon as the action is queued; the outcome arrives as a new state.
subscribe()Opens a subscription that yields the store's states one by one.
close()Unmounts the store and cancels the work it still has running.

How each platform exposes them:

MemberSwiftKotlinTypeScript
Mounttry InquiryStore.mount(runtime: sdk)InquiryStore.mount(sdk)InquiryStore.mount(sdk)
State@Published property state, publisher $statestate: StateFlow<InquiryUiModel>, current value state.valueGetter state
Dispatchtry store.dispatch(action: .close)store.dispatch(InquiryAction.Close)store.dispatch(InquiryAction.close)
Subscribetry store.subscribe() returns an InquirySubscriptionstore.subscribe() returns an InquirySubscriptionstore.subscribe() returns an InquirySubscription; store.subscribe(listener) registers a listener and returns its remover
Closestore.close(), also on deinitstore.close(), the store is AutoCloseablestore.close(), also [Symbol.dispose]()

dispatch takes an optional second argument, a LogContext, that ties the action's journal entries to an interaction; see Logging.

On iOS, state updates arrive on the main actor, and every store is an ObservableObject. On Android, state is a StateFlow you can collect in Compose with collectAsState(). On the web, a listener passed to subscribe takes no arguments; read store.state inside it.

On iOS and Android the store keeps state current through a subscription of its own, opened at mount; on the web it opens one while a listener is subscribed. If that stream fails, the store keeps the error: streamError, a @Published Error? on iOS and a StateFlow<Throwable?> on Android; on the web reading state throws it.

Swift
let vault = try VaultStore.mount(runtime: sdk)
let records = vault.state.records

Subscriptions

subscribe() takes no arguments and returns a subscription. Its asynchronous next() returns a state each time you call it:

  • The first next() returns the current state, every later one the next changed state. When the state changes several times between two calls, next() returns only the latest state.
  • After the store is unmounted, by close() or by shutdown() of the runtime, next() returns no value: nil in Swift, null in Kotlin and undefined on the web.
  • Holding a subscription does not keep the store mounted. Release the subscription when you no longer read it; the store stays mounted.

Each store has its own subscription type, named after the store: InquirySubscription, VaultSubscription and so on. Iterating a subscription releases it when the iteration ends.

Platformnext()ReleaseIterate
Swifttry await subscription.next()release(), also on deinitfor try await state in subscription
Kotlinsubscription.next(), a suspendclose(), it is AutoCloseablesubscription.asFlow()
Webawait subscription.next({ signal })dispose() or [Symbol.dispose]()for await (const state of subscription)
Swift
let subscription = try vault.subscribe()
for try await state in subscription {
    render(state.records)
}

Locale changes

A store's state carries text that is already localized. When the locale saved in the SDK changes, for example after a language pick in the inquiry, every mounted store publishes its state again in the new language. You do not need to reload anything. A change of the device language while the runtime runs does not make the stores publish again. See Localization.

Store errors

mount, dispatch and subscribe fail with a CoreError. The call throws the platform's SDK error, FolioSDKError on iOS and the web and FolioSDKException on Android, whose message is the text of the CoreError.

CoreErrorMeaning
UnmountedThe store is no longer mounted because its runtime was shut down. For a store you closed, see below.
StoreOverflowThe action could not be queued: the runtime's action queue, shared by all stores mounted on it, is full, or the runtime has stopped.
InvalidInputA value you passed cannot be decoded, or the LogContext passed to dispatch belongs to another runtime; see Logging.
ArenaFullNo more stores can be mounted on this runtime.
SourceUnavailableThe store's last state was computed from inputs that are no longer active and no newer state is ready yet. Reported by a state read (mount, and on the web reading state while no listener is subscribed) and by dispatch while the store's inputs are revoked and no renewed state has been accepted yet; the store stays mounted, and dispatch succeeds again once it has taken the renewed state.

The action queue holds at most 256 actions. Actions of InquiryStore and MrtdStore do not wait in it: they go straight to the store loop, so a full queue never rejects them. The one exception is the inquiry's camera frame, the intent PreviewFrame: it waits in the queue, and when the queue is full the frame is dropped and dispatch does not throw.

After close(), dispatch and subscribe throw: FolioSDKError with the message "Handle already released" on iOS and IllegalStateException with the message "Handle has been closed" on Android. On the web, dispatch and subscribe() throw an Error such as VaultStore has been disposed, while state keeps returning the last state and subscribe(listener) returns a remover that does nothing. close() itself can be called more than once.

A failure of the work behind an action, such as a rejected request, is not thrown from dispatch. It appears in the store's state. Each store page describes where.

Static functions

The SDK also has functions that need no runtime, such as text lookup and the country list. They are static members of FolioSdk, so you call them on the class, not on an instance. In Swift every one of them is declared throws.

SwiftKotlinTypeScript
try FolioSdk.localize(key:locale:)FolioSdk.localize(key, locale)FolioSdk.localize(key, locale)
try FolioSdk.localizeWithArgs(key:locale:args:)FolioSdk.localizeWithArgs(key, locale, args)FolioSdk.localizeWithArgs(key, locale, args)
try FolioSdk.localizePlural(group:count:locale:)FolioSdk.localizePlural(group, count, locale)FolioSdk.localizePlural(group, count, locale)
try FolioSdk.localizeCount(group:count:locale:)FolioSdk.localizeCount(group, count, locale)FolioSdk.localizeCount(group, count, locale)
try FolioSdk.localizeWithCount(key:group:count:locale:)FolioSdk.localizeWithCount(key, group, count, locale)FolioSdk.localizeWithCount(key, group, count, locale)
try FolioSdk.formatFileSize(bytes:locale:)FolioSdk.formatFileSize(bytes, locale)FolioSdk.formatFileSize(bytes, locale)
try FolioSdk.availableLocales()FolioSdk.availableLocales()FolioSdk.availableLocales()
try FolioSdk.countries()FolioSdk.countries()FolioSdk.countries()
try FolioSdk.country(code:)FolioSdk.country(code)FolioSdk.country(code)
try FolioSdk.parseCountry(code:)FolioSdk.parseCountry(code)FolioSdk.parseCountry(code)
try FolioSdk.countryFlag(country:)FolioSdk.countryFlag(country)FolioSdk.countryFlag(country)
try FolioSdk.countryByPhone(phone:)FolioSdk.countryByPhone(phone)FolioSdk.countryByPhone(phone)
try FolioSdk.subdivisions(country:)FolioSdk.subdivisions(country)FolioSdk.subdivisions(country)
try FolioSdk.phoneDialCode(country:)FolioSdk.phoneDialCode(country)FolioSdk.phoneDialCode(country)
try FolioSdk.formatPhoneNational(raw:country:)FolioSdk.formatPhoneNational(raw, country)FolioSdk.formatPhoneNational(raw, country)
try FolioSdk.phoneCountryOptions()FolioSdk.phoneCountryOptions()FolioSdk.phoneCountryOptions()
try FolioSdk.currencies()FolioSdk.currencies()FolioSdk.currencies()
try FolioSdk.addressLayout(format:locale:)FolioSdk.addressLayout(format, locale)FolioSdk.addressLayout(format, locale)
try FolioSdk.addressForm(address:fallbackCountryCode:locale:)FolioSdk.addressForm(address, fallbackCountryCode, locale)FolioSdk.addressForm(address, fallbackCountryCode, locale)
try FolioSdk.addressEmpty()FolioSdk.addressEmpty()FolioSdk.addressEmpty()
try FolioSdk.addressChangeCountry(address:country:)FolioSdk.addressChangeCountry(address, country)FolioSdk.addressChangeCountry(address, country)
try FolioSdk.addressChangePart(address:key:value:)FolioSdk.addressChangePart(address, key, value)FolioSdk.addressChangePart(address, key, value)
try FolioSdk.addressDisplay(address:)FolioSdk.addressDisplay(address)FolioSdk.addressDisplay(address)
try FolioSdk.applyDateInputMask(input:format:)FolioSdk.applyDateInputMask(input, format)FolioSdk.applyDateInputMask(input, format)
try FolioSdk.decodeLink(link:)FolioSdk.decodeLink(link)FolioSdk.decodeLink(link)
try FolioSdk.isValidEmail(value:)FolioSdk.isValidEmail(value)FolioSdk.isValidEmail(value)
try FolioSdk.extractRecoveryCode(text:)FolioSdk.extractRecoveryCode(text)FolioSdk.extractRecoveryCode(text)
try FolioSdk.loggingConfig(consoleLevel:)FolioSdk.loggingConfig(consoleLevel)FolioSdk.loggingConfig(consoleLevel)
try FolioSdk.mrtdDataGroupsStandard()FolioSdk.mrtdDataGroupsStandard()FolioSdk.mrtdDataGroupsStandard()
try FolioSdk.mrtdDataGroupsAll()FolioSdk.mrtdDataGroupsAll()FolioSdk.mrtdDataGroupsAll()
try FolioSdk.inquiryTheme()FolioSdk.inquiryTheme()FolioSdk.inquiryTheme()
try FolioSdk.barcodeImage(barcodeType:value:)FolioSdk.barcodeImage(barcodeType, value)FolioSdk.barcodeImage(barcodeType, value)
try FolioSdk.computeMetrics(format:data:width:height:strideYOpt:)FolioSdk.computeMetrics(format, data, width, height, strideYOpt)FolioSdk.computeMetrics(format, data, width, height, strideYOpt)
try FolioSdk.rotateImage(argbData:width:height:direction:)FolioSdk.rotateImage(argbData, width, height, direction)FolioSdk.rotateImage(argbData, width, height, direction)
try FolioSdk.sdkBuildInfo()FolioSdk.sdkBuildInfo()FolioSdk.sdkBuildInfo()
try FolioSdk.iconAsset(icon:)FolioSdk.iconAsset(icon)FolioSdk.iconAsset(icon)
try FolioSdk.statusPalette()FolioSdk.statusPalette()FolioSdk.statusPalette()

iconAsset returns the asset name of an Icon, such as bed for Bed, and statusPalette the colors of the document status styles, for an app that draws a document card itself. The Folio documents stores need neither of them.

Your app finds an icon image by its asset name:

On iOS, the images are template images in the resource bundle of the FolioInquiryUI library, FolioResourceAssets.bundle. Each is named resource- followed by the asset name, such as resource-bed. Link FolioInquiryUI to use them.

Swift
import FolioInquiryUI
import FolioSDK
import SwiftUI

let name = try FolioSdk.iconAsset(icon: .bed)
let icon = Image("resource-" + name, bundle: FolioResourceAssets.bundle)

Where each group is described:

  • Text, locales, countries, phone numbers, addresses, date input masks and links: Localization, with the exact signatures.
  • currencies returns the CurrencyCatalog, whose items hold one CurrencyInfo { currency, name, minorUnitDigits } per Currency. name is a LocalizedText: its values map a locale to the name, with the ISO 4217 name under the key _. minorUnitDigits is absent when ISO 4217 assigns the currency no minor unit.
  • isValidEmail checks the shape of an email address, ignoring surrounding whitespace. extractRecoveryCode returns the first recovery code (four groups of four letters or digits joined by hyphens, read in upper case) found in a text, or an empty string.
  • loggingConfig: Logging.
  • mrtdDataGroupsStandard and mrtdDataGroupsAll: NFC chip reading.
  • inquiryTheme: Web setup.
  • sdkBuildInfo: Versions and build info.

Image helpers

Three functions work on images without a runtime. When the input is not valid, they fail with an ImageProcessingError, thrown as the platform's SDK error.

FunctionReturnsWhat it does
barcodeImage(barcodeType, value)ImageContentEncodes value as a barcode of the BarcodeType and returns it as a PNG, one pixel per module and without a quiet zone: scale it up without smoothing and draw the light border yourself.
computeMetrics(format, data, width, height, strideYOpt)ImageMetricsMeasures the luminance of a camera frame in the PixelFormat RGBA, NV12 or NV21: mean brightness, variance, standard deviation, median, minimum, maximum and dynamic range. A strideYOpt of 0 means the default row stride.
rotateImage(argbData, width, height, direction)RotatedImageResultRotates ARGB pixel data, four bytes per pixel, by a RotationDirection. The result is an object: data(), width() and height() return the rotated pixels and their size.

BarcodeType is QR, CODE128, PDF417, AZTEC, DATA_MATRIX, CODE39, CODE93, CODABAR, INTERLEAVED2_OF5, EAN13, EAN8, UPC_A or UNKNOWN. QR carries its error correction level, a QrLevel: L, M, Q or H. Swift writes the cases in lower camel case: .qr(level: .m), .dataMatrix, .upcA. Kotlin uses the sealed class BarcodeType: BarcodeType.Qr(QrLevel.M), BarcodeType.DataMatrix, BarcodeType.UpcA. TypeScript uses the tagged union with the type values above and the builders BarcodeType.qr(QrLevel.M) and BarcodeType.dataMatrix. barcodeImage throws for an empty value, for UNKNOWN and for a value the barcode type cannot encode.

RotationDirection is NONE, CLOCKWISE90, COUNTER_CLOCKWISE90 or ROTATE180 (.none, .clockwise90, .counterClockwise90 and .rotate180 in Swift). Release a RotatedImageResult when you have read it: release() in Swift, close() in Kotlin and dispose() on the web.

On this page