Card issuers (banks, fintechs) can let users add a payment card to Apple Wallet from inside the Wallet app itself. The user opens Wallet, taps the add button and sees the issuer’s cards listed under "From apps on your iPhone" - without ever launching the issuer’s app. Apple implements this through a pair of app extensions called issuer provisioning extensions, and Codename One can generate them for you from build hints with no native code in your project.

This is distinct from the in-app flow where your app shows an "Add to Apple Wallet" button. The extension flow starts inside the Wallet app, runs in a separate process and usually runs while your app isn’t running at all.

How the extension works

Apple defines two extension points:

  • A non-UI extension that Wallet queries for status and the list of available cards, and that produces the encrypted provisioning payload when the user adds a card. Wallet gives it 100ms to answer the status query, so it can’t perform network calls or heavy work there.

  • An optional authorization UI extension that presents a login screen inside Wallet when the non-UI extension reports that authentication is required.

Because the extensions run in a separate process, they can’t call into your Java code. The Codename One integration bridges this in two ways:

  1. Your app pre-publishes the card list, card art and an auth token through com.codename1.payment.WalletExtension. The data lands in a shared App Group container where the generated extension reads it instantly.

  2. The one step that genuinely needs the issuer’s backend - producing encryptedPassData from the certificates and nonce that Apple hands the extension - is performed by an HTTPS endpoint you host. The generated extension POSTs the request there and relays the response to Wallet.

Prerequisites from Apple

Before any of this works you need approvals that only Apple and your card network can grant:

  • The com.apple.developer.payment-pass-provisioning entitlement is restricted. Apple grants it per app on request (see the In-App Provisioning documentation on the Apple developer site). After approval, enable it under the App ID’s Additional Capabilities tab for the app and for each extension App ID.

  • Each extension needs its own App ID (for example com.mybank.app.WalletNonUIExtension) and its own provisioning profile carrying the entitlement.

  • The card network/PNO pass metadata must list the extension App IDs in associatedApplicationIdentifiers, otherwise Wallet never invokes the extensions.

  • The extensions require iOS 14. The generated extension targets set their own deployment target, so your app can still target an older iOS version.

Mode 1: Generate the extensions from build hints

Add these build hints to the iOS build:

codename1.arg.ios.wallet.extension=true
codename1.arg.ios.wallet.appGroup=group.com.mybank.app
codename1.arg.ios.wallet.issuerEndpoint=https://api.mybank.com/wallet/provision

That generates the non-UI extension, wires it into the Xcode project as an embedded extension target and injects the App Group into the app and extension entitlements. To also generate the login UI extension add:

codename1.arg.ios.wallet.includeUI=true
codename1.arg.ios.wallet.authEndpoint=https://api.mybank.com/wallet/login

In Java, publish the user’s cards whenever they change (typically after login) and keep a fresh token published so Wallet can skip the login screen:

if (WalletExtension.isSupported()) {
    WalletExtension.setPassEntries(new WalletPassEntry[] {
        new WalletPassEntry()
            .identifier(card.getPrimaryAccountIdentifier())
            .title("My Bank Debit Card")
            .cardholderName(user.getFullName())
            .primaryAccountSuffix(card.getLast4())
            .paymentNetwork("Visa")
            .localizedDescription("My Bank Debit Card")
            .artPng(cardArt.getImageData())
    });
    WalletExtension.setRemotePassEntries(sameEntries); // Apple Watch list
    WalletExtension.setAuthToken(session.getToken());
    WalletExtension.setRequiresAuthentication(false);
}

Call WalletExtension.clear() on logout. The card art must be a PNG without personally identifiable information (Apple requires square corners and no full card number). The extension automatically filters out cards that are already provisioned on the device or the paired watch.

The issuer endpoint

When the user adds a card, the generated extension POSTs JSON to the ios.wallet.issuerEndpoint URL:

{
  "certificates": ["base64...", "base64..."],
  "nonce": "base64...",
  "nonceSignature": "base64...",
  "cardIdentifier": "the WalletPassEntry identifier",
  "authToken": "the token published via setAuthToken"
}

The token is also sent as an Authorization: Bearer header. Your backend performs the network-specific encryption (this always happens server side - the keys never live on the device) and responds with:

{
  "activationData": "base64...",
  "encryptedPassData": "base64...",
  "ephemeralPublicKey": "base64..."
}

For the RSA_V2 scheme return wrappedKey instead of ephemeralPublicKey.

The login UI extension

When you can’t guarantee a fresh token, report setRequiresAuthentication(true) and enable the UI extension. It shows a minimal username/password form inside Wallet, POSTs {"username", "password"} to the ios.wallet.authEndpoint URL, expects {"token"} back and stores it where the non-UI extension picks it up. If the generated form doesn’t fit your brand or login model, use the injection hints below or bring your own extension (Mode 2).

Customizing the generated code

The generated Objective-C contains marker comments at every interesting point, and each marker has a matching build hint that injects your snippet there: ios.wallet.nonuiImportsInject, ios.wallet.statusInject, ios.wallet.passEntriesInject, ios.wallet.remotePassEntriesInject, ios.wallet.generateRequestInject (runs before the POST - the mutable payload dictionary is in scope, so you can add fields your backend expects), ios.wallet.generateResponseInject, ios.wallet.uiImportsInject, ios.wallet.uiViewDidLoadInject, ios.wallet.uiAuthRequestInject and ios.wallet.uiAuthResponseInject.

Xcode build settings of the extension targets can be overridden with ios.wallet.nonui.buildSettings.SETTING=value and ios.wallet.ui.buildSettings.SETTING=value - useful in local builds for DEVELOPMENT_TEAM or CODE_SIGN_STYLE.

Provisioning profiles for cloud builds

Cloud device builds sign each extension with its own profile. Either place the .mobileprovision files under common/src/main/resources and name them in build hints:

codename1.arg.ios.wallet.nonuiProvisioningProfile=WalletNonUI.mobileprovision
codename1.arg.ios.wallet.uiProvisioningProfile=WalletUI.mobileprovision

Alternatively, host them at URLs the build server can reach and supply ios.wallet.nonuiProvisioningURL / ios.wallet.uiProvisioningURL instead. Profiles contain no private keys, so bundling them in resources is safe; the build keeps them out of the final app bundle. The build validates that each profile matches your distribution certificate and actually carries the payment-pass-provisioning entitlement, and fails with an actionable message when it doesn’t.

In local builds ("ios-source" target) no profile hints are needed - you sign the generated Xcode project in Xcode as usual.

Mode 2: Bring your own extension

If you already have Xcode extension targets - from an issuer SDK vendor or an existing native app - skip the ios.wallet.* hints and drop each extension into the generic app extension mechanism instead. Create ios/app_extensions/MyWalletExtension/ in your project containing:

  • The extension’s source files (.m, .h, .swift)

  • Info.plist with the NSExtension dictionary

  • MyWalletExtension.entitlements

  • Optional buildSettings.properties with Xcode build settings, one per line (set IPHONEOS_DEPLOYMENT_TARGET=14.0 for Wallet extensions)

  • The extension’s own .mobileprovision for cloud device builds - see below

Each folder becomes an embedded extension target with bundle id <your package>.<folder name>. This mechanism isn’t Wallet-specific - it embeds any iOS app extension type.

Signing a brought-in extension

Apple signs an app extension against its own App ID. The app’s provisioning profile covers the app’s bundle id and nothing beneath it, so every extension needs its own App ID and its own profile - two extra of each for a Wallet pair. Supply none and the extension target inherits the app’s profile, and Xcode fails the build with:

Provisioning profile "MyApp_Distribution" has app ID "com.mybank.app", which does not
match the bundle ID "com.mybank.app.WalletNonUIExtension"

Codename One refuses that build before it’s sent, naming the extension and the bundle id its App ID has to match. To fix it, create an App ID for <your package>.<folder name> in the Apple Developer portal - for a Wallet extension with the com.apple.developer.payment-pass-provisioning entitlement Apple grants on request, and with the same App Group as the app - generate a provisioning profile for it against the same certificate as the app, and supply it in one of three ways:

  1. Place the .mobileprovision file inside ios/app_extensions/MyWalletExtension/. It’s picked up automatically and kept out of the app bundle. Profiles carry no private keys, so committing one is safe.

  2. Point at it from codenameone_settings.properties, which suits a profile that lives outside the repository:

    codename1.ios.debug.appext.MyWalletExtension.provision=${user.home}/certs/MyWalletExtension_dev.mobileprovision
    codename1.ios.release.appext.MyWalletExtension.provision=${user.home}/certs/MyWalletExtension_dist.mobileprovision

    The unqualified codename1.ios.appext.MyWalletExtension.provision is the fallback for both build types.

  3. Host it and name the URL in ios.appext.MyWalletExtension.provisioningURL (or the ios.debug.appext…​ / ios.release.appext…​ variants).

The build validates the profile it’s given: it must match the app’s signing certificate, cover the extension’s bundle id, be current, and grant the App Groups and the payment-pass-provisioning entitlement the extension’s .entitlements asks for. Each of those fails with a message naming the extension, rather than as an Xcode code signing error minutes into the build.

A wildcard App ID (com.mybank.*) is the one case that needs no extra profile - it’s already covering the extension bundle ids, and the build lets it through. Wallet extensions can’t use one: the payment-pass-provisioning entitlement is granted per explicit App ID.

Local "ios-source" builds need none of this - you sign the generated Xcode project in Xcode as usual.

Testing

Wallet extensions can’t be exercised in the iOS simulator’s Wallet app; testing the full flow requires a device, a provisioning profile with the entitlement and a card network sandbox. What you can verify earlier:

  • A local "ios-source" build with the hints produces the WalletNonUIExtension/WalletUIExtension targets - open the generated Xcode project and build.

  • WalletExtension.isSupported() returns false in the simulator and on other platforms, and all publish calls are safe no-ops there, so the Java code needs no platform guards beyond the one shown above.