light-mode-image
Learn
Management & OperationsMigration Guides

iOS Holder SDK v6.0.0 Migration Guide

A comprehensive guide to migrating to iOS Holder SDK v6.0.0, covering breaking changes, new features, and step-by-step migration instructions.

Overview

This guide provides a comprehensive overview of the changes introduced in the iOS Holder SDK v6.0.0, including breaking changes, new features, and migration steps.

This release focuses on strengthening trust at issuance, improving predictability in credential handling, and increasing consistency across platforms. It introduces optional SDK Tethering to connect the SDK to your MATTR VII tenant and optional Wallet Attestation support, enabling issuers to verify wallet integrity before issuing credentials. Together, these changes make holder-side integrations more reliable and easier to reason about in production environments.

SDK Tethering (and Wallet Attestation, which builds on it) is optional in this release. You opt in by passing a platformConfiguration when initializing the SDK. If you omit it, the SDK skips registration, tethering, and wallet attestation, and existing integrations continue to work. We expect to make SDK Tethering required in an upcoming release, so we recommend adopting it now to prepare.

Key Features

  • SDK Tethering (optional): The iOS Holder SDK can now be tethered to a MATTR VII tenant, tying each SDK/app instance to your tenant. This allows you to view details about registered and active app instances directly from your tenant for operational insights. SDK Tethering also establishes a remote management channel that we expect to extend in the future, such as remote syncing of trusted issuer lists and eventing. Tethering is enabled by providing a platformConfiguration at initialization.
  • Wallet Attestation support (optional): Building on the SDK Tethering channel, the iOS Holder SDK now supports Wallet Attestation, allowing your holder application to prove to issuers that it is a trusted wallet before credentials are issued. When an issuer requires it, the SDK uses the tethering connection to obtain wallet attestation tokens from your MATTR VII tenant. Wallet Attestation requires SDK Tethering to be configured.
  • Stronger application identity during issuance: Pre-authorized credential issuance flows now pass the application's client_id, ensuring the holder is accurately represented when interacting with issuers. This improves compatibility with issuers applying stricter controls.
  • More predictable credential retrieval results: Credential retrieval responses are now more structured and deterministic, explicitly identifying success or failure with guaranteed fields per result type.
  • Cross-platform alignment: Naming and response structures have been aligned with the Android Holder SDK, minimizing divergence for teams maintaining cross-platform applications.
  • Improved biometric and storage lifecycle handling: Edge cases in biometric authentication, storage lifecycle, and app lifecycle events (such as prewarming or reinstall scenarios) have been addressed.
  • General stability and performance improvements: Multiple refinements reduce integration friction, increase consistency, and improve overall reliability.

Breaking Changes

This section outlines the breaking changes introduced in v6.0.0 that require updates to your existing implementation:

#ChangeImpact
1initialize and initializeAppExtension are now asynchronousAdd await to all call sites and call these methods from an asynchronous context.
2Pre-authorized code flow now uses the application's client_id instead of a default identifierEnsure your application has a valid configured client_id and issuers recognize it.
3Credential retrieval result shape updated to explicitly identify success or failure with guaranteed fieldsUpdate result parsing logic to use success/failure branching.
4MobileCredentialAuthenticationOption renamed to DeviceAuthenticationOption (and the mobileCredentialAuthenticationOption: parameter renamed to deviceAuthenticationOption:)Update all imports, type references, and session-creation parameter names.
5OnlinePresentationSession.matchedCredentials is now the getMatchedCredentials() method and returns a non-optional arrayReplace property access with the method call and remove optional handling.
6VerificationResult renamed to MobileCredentialVerificationResult, with .reason renamed to .failureTypeUpdate all type references, rename .reason to .failureType, and remove use of VerificationFailedReason.
7Wallet Attestation introduces new error cases on MobileCredentialHolderError and RetrieveCredentialErrorTypeUpdate error handling, logging, analytics, and support diagnostics to account for the new wallet attestation error cases.
8challenge in requestMobileCredentials is now optional to align with AndroidRemove assumptions that challenge must always be supplied.
9MobileCredential.claims and MobileCredentialMetadata.claims are now non-optional, and credentials with empty claims are rejectedRemove optional chaining on claims and handle the decoding error where credentials are retrieved or added.
10MobileCredentialResponse.credentials and MobileCredentialResponse.credentialErrors are now non-optional arraysRemove optional handling on these properties.
11docType is now serialized under the JSON key docType (previously doctype)Update any persisted or parsed serialized forms to use docType.
12A .none case was added to UserAuthenticationType, and device-key auth types are now non-optionalPass DeviceKeyAuthenticationPolicy(type: .none) instead of nil to disable authentication for a device key.
13The claims property on OfferedCredential is now optional ([Claim]?), aligning with the OID4VCI 1.0 specificationHandle the case where OfferedCredential.claims is nil, indicating the offer contains no claim data.
14New required tokenEndpointAuthMethodsSupported, authorizationServerIssuer, and nonceEndpoint properties added to DiscoveredCredentialOfferSupply the new arguments if you construct this type directly, and update any stored representations.
15MobileCredentialVerificationFailureType and TrustedCertificateVerificationFailureType now serialize as a {type, message} object instead of a plain raw-value stringUpdate any storage or transport layer that persists or forwards these serialized values.
16Remote mobile (app-to-app) verification now maps the backend IssuerNotTrusted failure to MobileCredentialVerificationFailureType.TrustedIssuerCertificateNotFoundRemove any IssuerNotTrusted handling and handle TrustedIssuerCertificateNotFound instead.
17The internal storage location for app extension logs has changedNo code change required (the same getCurrentLogFilePath(appGroup:) method is used), but app extension logs written before the upgrade become inaccessible.

SDK initialization does not require platform/tenant configuration in this release. The platformConfiguration parameter is optional. Provide it only if you want to enable SDK Tethering and Wallet Attestation. See Update SDK initialization.

Migration Steps

Make initialize calls asynchronous

The initialize and initializeAppExtension methods are now asynchronous. Add await to all call sites and call them from an asynchronous context:

- try MobileCredentialHolder.shared.initialize(instanceID: instanceID)
+ try await MobileCredentialHolder.shared.initialize(instanceID: instanceID)

(Optional) Create a holder application on your MATTR VII tenant

SDK Tethering is optional in this release. If you do not need it, you can skip this step and the next one. The SDK continues to function without a platformConfiguration.

To enable SDK Tethering, the iOS Holder SDK connects to a MATTR VII tenant. Tethering gives you access to a centralized view of registered and active app instances in MATTR VII, the optional Wallet Attestation feature, and other centralized management capabilities we plan to deliver over this channel.

We expect SDK Tethering to become required in an upcoming release. We recommend adopting it now to prepare, even if you do not yet use Wallet Attestation.

To register your iOS application, make a request to create a holder application:

Request
POST /v1/holder/applications
Request body
{
    "name": "My iOS Holder Application",
    "clientId": "your-wallet-client-id",
    "type": "ios",
    "bundleId": "com.yourcompany.holderapp",
    "teamId": "YOUR_APPLE_TEAM_ID"
}
  • name: A unique name to identify your holder application.
  • clientId: The OAuth 2.0 client_id that identifies your wallet application. This value is included in attestation JWTs and must match the client_id configured on the issuer's Authorization Server.
  • type: Must be ios for an iOS application.
  • bundleId: The Bundle ID of your iOS app (must match your Xcode project configuration).
  • teamId: Your Apple Developer Team ID.

Once created, your holder application will be able to use the SDK and interact with the MATTR VII platform (for example, to obtain attestation tokens).

The response will include a unique id for your application, which must be used when initializing the SDK so that it can correctly identify and authenticate your application.

The clientId you configure here is the same value you must:

  1. Pass as the clientId parameter when calling retrieveCredentials.
  2. Register with each issuer you intend to interact with, so the issuer can identify and trust requests coming from your wallet application.

(Optional) Enable tethering at SDK initialization

To enable SDK Tethering, pass a platformConfiguration when initializing the SDK. This parameter is optional. Omit it to initialize the SDK without tethering:

- try await MobileCredentialHolder.shared.initialize(instanceID: instanceID)
+ let platformConfig = PlatformConfiguration(
+     tenantHost: URL(string: "https://your-tenant.vii.mattr.global")!,
+     applicationId: "1ef1f867-20b4-48ea-aec1-bea7aff4964c"
+ )
+ try await MobileCredentialHolder.shared.initialize(
+     instanceID: instanceID,
+     platformConfiguration: platformConfig
+ )
  • tenantHost: The URL of your MATTR VII tenant. This must be the tenant where your holder application is configured.
  • applicationId: The id of your configured MATTR VII holder application.

When platformConfiguration is omitted, the SDK skips registration and tethering, and wallet attestation is not available.

Handle new error cases

This release adds new cases to MobileCredentialHolderError, which is a breaking change that requires updates to exhaustive switch statements regardless of whether you enable SDK Tethering:

  • invalidLicense: Thrown when the SDK license is invalid or expired.
  • failedToRegister: Thrown when app registration with the MATTR VII tenant fails.
  • invalidWalletAttestation: Thrown when the authorization server rejects the wallet attestation during credential retrieval.
  • calledFromAppExtension: Thrown when an SDK API that is unavailable in app extensions is called from an app extension at runtime.

If you enable SDK Tethering and Wallet Attestation, the following per-credential failure types are also added to RetrieveCredentialErrorType:

  • walletAttestationFailed: Returned when wallet attestation generation fails.
  • invalidWalletAttestation: Returned when the attestation is rejected by the credential endpoint.

The retrieveCredentials method may now throw invalidWalletAttestation and return the new per-credential failure types. Wallet attestation errors only occur when the SDK is tethered and an issuer requires attestation.

Update your error handling, logging, analytics, and support diagnostics to account for these new error cases. Ensure any SDK API calls from app extensions are wrapped in appropriate availability checks to prevent runtime errors.

Update client_id configuration

Previously, the client_id passed to retrieveCredentials was not shared with the issuer during the pre-authorized code flow, so any value would work. This is no longer the case. The SDK now presents the client_id to the issuer as part of wallet attestation, and the issuer validates it against its list of trusted wallet providers.

To prepare for this change:

  1. Coordinate with the issuer to register your wallet application as a trusted wallet provider. The issuer will provide you with a client_id that identifies your application.
  2. Pass the issuer-provided client_id when calling retrieveCredentials.
  let results = try await holder.retrieveCredentials(
      credentialOffer: offer,
-     clientId: "any-value",
+     clientId: "issuer-provided-client-id"
  )

Issuance flows that previously worked with an arbitrary client_id will fail if the issuer requires a trusted wallet provider. Ensure you have coordinated with each issuer and obtained the correct client_id before upgrading. Test direct issuance flows to confirm credentials are issued successfully.

Update credential retrieval result handling

RetrieveCredentialResult has been converted from a struct with optional fields to an enum with explicit .success and .failure cases. Each case carries guaranteed associated values, removing ambiguity from result handling.

The retrieveCredentials function returns [RetrieveCredentialResult], an array with one result per offered credential. Update your iteration logic to use pattern matching:

  let results = try await holder.retrieveCredentials(options)
  for result in results {
-     if let credentialId = result.credentialId {
-         // Use result.docType and credentialId
-     } else if let error = result.error {
-         // Use result.docType and error
-     }
+     switch result {
+     case .success(let docType, let credentialId):
+         // docType and credentialId are guaranteed
+     case .failure(let docType, let error):
+         // docType and error are guaranteed
+     }
  }

Update tests and any downstream logic that checked for nil fields.

Rename MobileCredentialAuthenticationOption to DeviceAuthenticationOption

Update all imports, type references, and configuration objects:

- let authOption: MobileCredentialAuthenticationOption = .biometryCurrentSet
+ let authOption: DeviceAuthenticationOption = .biometryCurrentSet

Update matchedCredentials handling for online presentation

The optional matchedCredentials property on OnlinePresentationSession has been replaced by a getMatchedCredentials() method that returns a non-optional array. Replace property access (and any optional handling) with the method call:

- if let matched = session.matchedCredentials {
-     // Handle matched credentials
- } else {
-     // Handle nil case
- }
+ // getMatchedCredentials() always returns an array (may be empty)
+ let matched = session.getMatchedCredentials()
+ if matched.isEmpty {
+     // Handle no matched credentials
+ } else {
+     // Handle matched credentials
+ }

Update VerificationResult to MobileCredentialVerificationResult

The VerificationResult type has been renamed to MobileCredentialVerificationResult and aligned structurally with Android. Its reason property has been renamed to failureType, typed directly as MobileCredentialVerificationFailureType? rather than the now-removed VerificationFailedReason wrapper:

- let result: VerificationResult = ...
+ let result: MobileCredentialVerificationResult = ...

- let failure = result.reason
+ let failure = result.failureType

Replace all references to VerificationResult with MobileCredentialVerificationResult, rename .reason to .failureType, and remove any usage of VerificationFailedReason. The same .reason.failureType rename applies to TrustedCertificateVerificationResult. You will also need to validate cross-platform result handling and update any downstream mapping logic.

Handle optional challenge in requestMobileCredentials

The challenge parameter is now optional in requestMobileCredentials. Review call sites and validation logic, and remove any app-side assumptions that challenge must always be supplied:

- // challenge was previously required
- let request = try holder.requestMobileCredentials(challenge: challenge, ...)
+ // challenge is now optional
+ let request = try holder.requestMobileCredentials(challenge: challenge, ...) // still works
+ let request = try holder.requestMobileCredentials(...) // also valid without challenge

Remove optional handling on claims

MobileCredential.claims and MobileCredentialMetadata.claims are now non-optional ([NameSpace: [ElementID: ElementValue]]), defaulting to [:] when namespaces are absent. Remove optional chaining on these accesses:

- let value = credential.claims?["org.iso.18013.5.1"]?["family_name"]
+ let value = credential.claims["org.iso.18013.5.1"]?["family_name"]

Retrieving or decoding a credential whose IssuerSigned data contains no namespaces (or a namespace with no claims) now fails with a decoding error instead of producing a credential with empty claims. Handle this decoding error where credentials are retrieved or added.

Remove optional handling on MobileCredentialResponse collections

MobileCredentialResponse.credentials and MobileCredentialResponse.credentialErrors are now non-optional arrays that default to empty, instead of optional arrays that could be nil. Remove optional handling on these properties:

- for credential in response.credentials ?? [] { ... }
+ for credential in response.credentials { ... }

Update docType serialization

The document type in RetrieveCredentialResult and OfferedCredential is now encoded under the JSON key docType instead of doctype. Update any code that persists or parses these serialized forms to use docType.

Update UserAuthenticationType handling

A .none case was added to UserAuthenticationType to explicitly disable user authentication. Consequently, DeviceKeyAuthenticationInfo.type and DeviceKeyAuthenticationPolicy.type are now non-optional UserAuthenticationType, and DeviceKeyAuthenticationPolicy(type:) no longer accepts nil. To disable authentication for a specific device key, pass .none instead of nil:

- let policy = DeviceKeyAuthenticationPolicy(type: nil)
+ let policy = DeviceKeyAuthenticationPolicy(type: .none)

Handle optional OfferedCredential.claims

The claims property on OfferedCredential is now optional ([Claim]?), aligning with the OID4VCI 1.0 specification. It is only populated for offers that contain claim data. Handle the case where claims is nil:

- let claims = offeredCredential.claims
+ let claims = offeredCredential.claims // may be nil when the offer contains no claim data
+ if let claims {
+     // Use the claims
+ }

Supply new DiscoveredCredentialOffer properties

The required tokenEndpointAuthMethodsSupported, authorizationServerIssuer, and nonceEndpoint properties have been added to DiscoveredCredentialOffer:

  • tokenEndpointAuthMethodsSupported: An array of authentication methods supported by the token endpoint.
  • authorizationServerIssuer: The issuer identifier of the authorization server.
  • nonceEndpoint: The authorization server nonce endpoint used for OID4VCI proof-of-possession.

The public initializer now requires these arguments and the encoded representation includes the corresponding fields. Most callers receive this type from discoverCredentialOffer and are not affected. If you construct or decode DiscoveredCredentialOffer directly, update your call sites and any stored representations.

Update failure-type serialization handling

MobileCredentialVerificationFailureType and TrustedCertificateVerificationFailureType now encode and decode as a {type, message} object instead of a plain raw-value string:

- "TrustedIssuerCertificateNotFound"
+ {"type": "TrustedIssuerCertificateNotFound", "message": "Trusted issuer certificate not found"}

If you persist or forward the serialized value of either failure type, update your storage or transport layer to produce and consume the new format.

Update issuer-trust failure handling in remote mobile verification

During remote mobile (app-to-app) verification, a backend IssuerNotTrusted failure reason is now decoded as MobileCredentialVerificationFailureType.TrustedIssuerCertificateNotFound instead of being surfaced as IssuerNotTrusted via the now-removed VerificationFailedReason wrapper. Remove any IssuerNotTrusted handling and update it to handle TrustedIssuerCertificateNotFound as the failure type for issuer trust failures:

- case .issuerNotTrusted:
+ case .trustedIssuerCertificateNotFound:
      // Handle issuer trust failure

Note the app extension log location change

The internal storage location for app extension logs has changed. Logs are still retrieved with the same getCurrentLogFilePath(appGroup:) method, so no code change is required. However, any app extension logs written before upgrading (up to the 48-hour retention window) will no longer be accessible after the upgrade. The location for main SDK logs remains unchanged.

How would you rate this page?

Last updated on

On this page