iOS SDK

Blueprint of the Moov Money reference app showing activity, a $25 payment with slide-to-confirm, and the payment ready to share.

The iOS SDK lets your customers send money to anyone from inside your app. You launch it from a single call site, it runs full screen, and you regain control when the user is done.

You supply your brand color and a short-lived token from your backend. The SDK resolves the user’s funding methods and then handles the payment or request. The other person claims or pays on the hosted page.

Requirements #

  • iOS 18.0 or later
  • Xcode 26.0 or later; earlier toolchains can’t compile the framework’s interface
  • Swift 6.0 language mode

Add the package #

The SDK resolves from a private GitHub repository, so Xcode needs a credential before it can fetch anything. Moov supplies a dedicated GitHub account for your team, invited as a read-only collaborator on moov-sdk/moov-money-ios.

  1. Accept the repository invitation emailed to that account.

  2. Sign in as that account and create a classic personal access token with the repo scope. A fine-grained token can’t read the repository. GitHub caps a classic token at 366 days and the build won’t warn you before it expires, so set a reminder.

  3. Add that account to Xcode under Settings → Accounts, using the token as the password.

  4. Open File → Add Package Dependencies and enter the package URL:

    https://github.com/moov-sdk/moov-money-ios.git
    

    Set the dependency rule to Up to Next Minor Version from 0.2.0-alpha01. Enter the version in full, suffix included: a range only matches prereleases when its lower bound is one, so 0.2.0 finds nothing. The SDK is pre-1.0, so minor versions can contain breaking changes. Pinning to the minor keeps an upgrade a deliberate act. 0.1.0 is unsupported.

  5. Add the MoovMoney library to your app target.

If you resolve packages from a Package.swift instead:

dependencies: [
    .package(
        url: "https://github.com/moov-sdk/moov-money-ios.git",
        .upToNextMinor(from: "0.2.0-alpha01")
    ),
]

Adding the MoovMoney library to your target embeds and signs everything the SDK needs into your app bundle, including the device intelligence framework Moov uses for fraud prevention. There’s nothing further to configure.

On CI and other command-line builds, xcodebuild can’t read Xcode’s account store. Add the same account and token to a git credential helper on each build machine:

git config --global credential.helper store
printf 'https://%s:%[email protected]\n' "$GITHUB_USERNAME" "$GITHUB_TOKEN" >> ~/.git-credentials

A machine with a warm package cache can build for months without this, then fail with could not read Username for 'https://github.com' on a fresh runner or after the cache is cleared.

Add usage descriptions #

The SDK needs three usage description strings in your app’s Info.plist. Without the first, the SDK won’t launch. Without the third, your App Store upload is rejected.

<key>NSFaceIDUsageDescription</key>
<string>Confirm it's you before sending a payment.</string>

<key>NSContactsUsageDescription</key>
<string>Find friends and family in your address book to pay.</string>

<key>NSCameraUsageDescription</key>
<string>Take a new profile photo.</string>

NSFaceIDUsageDescription lets the SDK confirm the device owner’s presence before submitting a payment. iOS terminates apps that reach LocalAuthentication without this key, so the SDK refuses to launch instead of crashing your app. You’ll see this as the missingEntitlement case in the next step.

NSContactsUsageDescription lets the SDK offer the device address book when the user chooses a recipient.

NSCameraUsageDescription is required even if you never show a camera interface. The SDK’s binary references AVCaptureDevice, and Apple’s static scan rejects the upload with ITMS-90683 when the string is absent. There’s no way to opt out of this one.

Check availability #

Consult MoovMoneySDK.availability before you offer your entry point. This is part of the integration contract rather than an optimization. It’s what lets you route to a web fallback instead of showing a button that can’t work.

import MoovMoney
import SwiftUI

struct SendMoneyButton: View {
    let configuration: MoovMoneyConfiguration
    let accessToken: String

    @State private var availability: MoovMoneyAvailability?
    @State private var isPresented = false

    var body: some View {
        Button("Send money") {
            guard case .available = availability else {
                showWebFallback()
                return
            }
            isPresented = true
        }
        .onAppear { availability = MoovMoneySDK.availability }
        .moovMoney(
            isPresented: $isPresented,
            accessToken: accessToken,
            configuration: configuration
        )
    }

    private func showWebFallback() { /* your existing web experience */ }
}

Read the verdict once and hold it, as above. Every .unavailable evaluation emits a diagnostic log line with no deduplication, so calling it from inside a SwiftUI body re-emits on every view update.

To tell the reasons apart, for example to distinguish an integration mistake from an old device:

if case .unavailable(let reason) = MoovMoneySDK.availability {
    switch reason {
    case .unsupportedOSVersion(let required):
        // The device is below the minimum iOS version, carried in `required`.
        // Offer your web experience.
        break
    case .missingEntitlement(let key):
        // Your Info.plist is missing `key`. See the previous step.
        break
    case .configurationInvalid(let detail):
        // `detail` names what is wrong with the configuration.
        break
    @unknown default:
        // Treat an unrecognized reason as unavailable and fall back.
        break
    }
}

availability is advisory. Both presentation entry points enforce the same checks independently, so a launch that shouldn’t happen presents nothing and logs it, rather than showing a broken experience.

Build a configuration #

Every field has a default, so the smallest usable configuration is empty. The SDK fetches the signed-in user’s eligible funding methods itself, scoped from the access token, the ones you registered against the participant. There’s nothing for you to resolve and pass in.

let configuration = MoovMoneyConfiguration()

To brand the experience and receive the SDK’s logs:

let configuration = MoovMoneyConfiguration(
    colors: MoovMoneyColors(primary: myBrandColor),
    logger: MoovMoneyLogger { entry in
        myLogger.log("[\(entry.category)] \(entry.message)")
    }
)

Wire up the logger. The SDK reports no outcome to your code, so these lines are the only signal your integration gets, including when the SDK refuses to launch. Pass the same configuration on every presentation: each one replaces the previous logger, and a configuration without one removes it. See Diagnostics below.

Present the experience #

You pass the access token on each presentation rather than storing it in the configuration, so the SDK always runs against a freshly minted credential. It’s a short-lived OAuth bearer token from your own token exchange, and it’s opaque to the SDK, never decoded on device. Your session model stays the authority on who can send or request.

Mint it from your backend with clientType set to device, for a participant that is active. It lasts about an hour, so mint one per presentation rather than caching it across launches. The SDK always calls production (api.moov.money). See sender authentication.

Attach the moovMoney modifier and drive it with a Binding, as shown under Check availability. The SDK sets the binding back to false when the flow finishes. There’s no result to handle.

Button("Send money") { isPresented = true }
    .moovMoney(
        isPresented: $isPresented,
        accessToken: accessToken,
        configuration: configuration
    )

The SwiftUI path can’t lock orientation. The amount-entry keyboard is portrait-only, so lock orientation at the app level if you need it enforced.

Diagnostics #

The SDK returns nothing to your code: no return value, no callback, no thrown error. A MoovMoneyLogger on your configuration is the only channel by which your integration learns what the SDK is doing.

That includes the launches it refuses. Nothing is presented and nothing is returned when:

  • the view controller you presented from is already presenting something
  • the access token is blank
  • the device fails the availability checks

Each entry carries a:

  • level (.debug, .info, .notice, or .error; switch with @unknown default)
  • category naming the subsystem ("SDK" for most lines, but also "Contacts", "Payouts", and others)
  • message, and
  • an optional underlying error.

Entries are Sendable, so you can forward one into a Task, buffer it, or ship it to a crash reporter asynchronously.

let appLog = Logger(subsystem: "com.example.bank", category: "MoovMoney")

let configuration = MoovMoneyConfiguration(
    logger: MoovMoneyLogger { entry in
        appLog.log("[\(entry.category)] \(entry.message)")
    }
)

It’s safe to leave installed in production. Values the SDK treats as sensitive are rendered <redacted> in message before they reach you, so no account number, phone number, amount, URL, response body, or identifier appears in that string. The error field is passed through as-is and carries no such guarantee.

Messages, categories, and levels are debugging aids rather than a stable contract, and they change between releases. Don’t build alerting or parsing on them.

The SDK also writes every line to os.log under subsystem io.moov.money, always on, including lines from before the first presentation:

xcrun simctl spawn booted log stream --level debug \
    --predicate 'subsystem == "io.moov.money"'

Troubleshooting #

SymptomCauseFix
The SDK opens, then closes at onceThe server refused the token (401). Logged at .notice as Session expired; dismissing the flow, whatever the actual reasonMint a fresh device token for an active participant, against production
Nothing presentsMissing NSFaceIDUsageDescription, a blank token, or presenting from a view controller that is already presentingCheck the logger; availability names the missing key
Xcode reports “No package collection found”The Xcode account can’t read the repository, or its token expiredRe-check the account and token from Add the package
Resolution that used to work now failsThe classic token expiredCreate a new one and replace it in Xcode and ~/.git-credentials
App Store upload rejected with ITMS-90683Missing NSCameraUsageDescriptionAdd it; no camera UI required

The binding returning to false after a refused token looks the same as a user canceling. The log line is the only way to tell them apart.

Theming #

MoovMoneyColors has one slot by design:

MoovMoneyColors(primary: .indigo)

The SDK owns the rest of its design language, drawing neutrals from iOS system semantic colors so they adapt to light and dark, Increase Contrast, and Smart Invert. Omit primary to use the SDK’s own default.

The SDK draws primary two ways. As a fill, it derives a readable label per appearance. As text and icons, it draws your color as-is on the system background. Supply a color with at least 4.5:1 contrast against both white and near-black, or an adaptive Color with a value for each appearance.

Disclose data collection #

The SDK ships a privacy manifest that merges into your app’s privacy report, so these entries appear in your App Store submission. Disclose them accordingly.

Data typeLinked to userTrackingPurpose
ContactsYesNoApp functionality
Photos or VideosYesNoApp functionality

Contacts covers the user’s address book, which the SDK uses to match contacts to Moov Money participants so the user can find and pay people they know.

Photos or Videos covers the profile photo a user chooses, which is stored against their account and used to render their avatar.

The manifest also declares the device and behavioral signals the SDK collects during the payment flow for fraud prevention. Those entries appear in your privacy report alongside the two above. The SDK performs no tracking as Apple defines it, and declares no tracking domains.

Known limitation #

If your app already embeds MobileIntelligence.framework, usually through a fraud or device intelligence SDK of its own, you’ll hit a duplicate framework error when you add Moov Money. Check your app bundle for that name before you start. The collision surfaces at build time, so it can’t ship by accident.

There’s no workaround to apply on your side. Contact your Moov integration contact and we’ll sort out the packaging with you.

Next steps #

  • Every public symbol carries documentation that ships inside the framework. Option-click any MoovMoney type or method in Xcode for full Quick Help, with no setup and no hosted site required.
  • See sender authentication for minting the access token.
  • The funding methods the SDK offers are the ones you registered against the participant. See create funding source and the ledger contract in the API reference.