Skip to content

Hold and show credentials

The wallet screen does all of this. If you build your own screens, the holder API is sdk.wallet:

HolderApi.kt
package samples
import com.wallet.WalletSdk
import com.wallet.model.PresentmentState
import com.wallet.model.SdkResult
import kotlinx.coroutines.flow.collect
/** The holder API behind `WalletScreen`, for apps that build their own screens. */
class HolderApi(private val sdk: WalletSdk, private val ui: Ui) {
interface Ui {
fun showCards(lines: List<String>)
fun showQr(payload: String)
fun askConsent(readerName: String?, requested: Any, onAnswer: (Boolean) -> Unit)
fun showError(message: String?)
fun done()
}
/** List what the wallet holds; the flow updates as credentials come and go. */
suspend fun listCredentials() {
sdk.wallet.credentials().collect { list ->
ui.showCards(list.map { "${it.displayName} ${it.docType} valid until ${it.validUntil}" })
}
}
/** One credential with its portrait and every field, labelled. */
suspend fun details(credentialId: String) {
val detail = sdk.wallet.details(credentialId)
detail?.claims?.forEach { println("${it.label}: ${it.value}") }
}
/** Receive a credential from an issuer's offer (a scanned QR or a link). */
suspend fun issue(offerUri: String) {
when (val preview = sdk.wallet.previewOffer(offerUri)) { // read-only, nothing redeemed yet
is SdkResult.Success -> println("Offer from ${preview.value.issuerName}: ${preview.value.credentialName}")
is SdkResult.Failure -> return ui.showError(preview.error.message)
}
when (val issued = sdk.wallet.issueFromOffer(offerUri)) { // after the holder confirms
is SdkResult.Success -> println("Issued ${issued.value.displayName}")
is SdkResult.Failure -> ui.showError(issued.error.message)
}
}
/** Present in person: show the QR, let the verifier connect, release only the approved fields. */
suspend fun present(credentialId: String? = null) {
val session = sdk.wallet.startPresentment(credentialId)
session.state.collect { state ->
when (state) {
is PresentmentState.Engaging -> state.qrPayload?.let(ui::showQr) // the verifier scans this
is PresentmentState.Consent -> ui.askConsent(state.readerName, state.requested) { approved ->
if (approved) session.approve(state.requested) else session.cancel()
}
is PresentmentState.Completed -> ui.done()
is PresentmentState.Failed -> ui.showError(state.error.message)
else -> {}
}
}
}
}

Issuers hand out credential offers: a QR code or a link (openid-credential-offer://…). The wallet previews the offer (issuer name, credential name), the holder confirms, and the credential is issued straight into the wallet, bound to a new key in the phone’s secure hardware. The wallet screen includes the scanner and the confirmation sheet.

  • The wallet speaks OpenID for Verifiable Credential Issuance (pre-authorised code flow). Any conforming issuer works; the demo’s competition licence comes from a real issuing platform.
  • Some issuers accept only Bearer tokens instead of DPoP-bound ones; DpopPolicy.ALLOW_BEARER_FALLBACK (the default) handles that per issuer.

Fields are labelled and formatted from the document type: dates as dates, booleans as Yes / No, the ISO sex code as Male / Female, the portrait as an image. Each document type has a card design drawn from the signed data; the drawn card is what the wallet, the consent sheet and the iOS Digital Credentials sheet show.

The Emirates ID card in the wallet
Drawn by the wallet from the signed data. Demo data is fictional.

Next: Present.