iOS SDK
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.
Accept the repository invitation emailed to that account.
Sign in as that account and create a classic personal access token with the
reposcope. 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.Add that account to Xcode under Settings → Accounts, using the token as the password.
Open File → Add Package Dependencies and enter the package URL:
https://github.com/moov-sdk/moov-money-ios.gitSet 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, so0.2.0finds 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.0is unsupported.Add the
MoovMoneylibrary 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.
Call present from your button’s action. It returns as soon as the
experience is on screen, and reports nothing back:
import MoovMoney
@IBAction func sendMoneyTapped() {
Task {
guard let accessToken = try? await myBackend.fetchMoovAccessToken() else {
return showTokenError()
}
MoovMoneySDK.present(
from: self,
accessToken: accessToken,
configuration: configuration
)
}
}
The UIKit path locks the experience to portrait itself.
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)categorynaming 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 #
| Symptom | Cause | Fix |
|---|---|---|
| The SDK opens, then closes at once | The server refused the token (401). Logged at .notice as Session expired; dismissing the flow, whatever the actual reason | Mint a fresh device token for an active participant, against production |
| Nothing presents | Missing NSFaceIDUsageDescription, a blank token, or presenting from a view controller that is already presenting | Check the logger; availability names the missing key |
| Xcode reports “No package collection found” | The Xcode account can’t read the repository, or its token expired | Re-check the account and token from Add the package |
| Resolution that used to work now fails | The classic token expired | Create a new one and replace it in Xcode and ~/.git-credentials |
| App Store upload rejected with ITMS-90683 | Missing NSCameraUsageDescription | Add 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 type | Linked to user | Tracking | Purpose |
|---|---|---|---|
| Contacts | Yes | No | App functionality |
| Photos or Videos | Yes | No | App 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
MoovMoneytype 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.