mcpField
F
financekit

financekit

github.com/dpearson2699数据与数据库1.2k3.1k 次安装13 周前更新
代码仓库

关于 financekit

面向 Apple 平台应用开发者的 FinanceKit 指南,介绍如何在用户授权后访问 Apple Wallet 中符合条件的账户、余额和交易数据。还涵盖 Wallet 订单、交易选择器与后台数据更新,并说明相关资格和授权限制。

主要功能

  1. 01请求金融数据授权
  2. 02查询账户、余额和交易
  3. 03使用历史记录进行增量同步
  4. 04通过 TransactionPicker 选择交易
  5. 05保存和查询 Wallet 订单
  6. 06注册金融数据后台更新

适用场景

  1. 01在预算应用中汇总账户余额
  2. 02同步交易以更新支出记录
  3. 03让用户临时选择要导入的交易
  4. 04将订单保存到 Apple Wallet

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill financekit

安装后在 Claude Code 中输入/financekit确认已加载。

附带文件evals/evals.jsonreferences/financekit-patterns.md

详细介绍

FinanceKit

访问 Apple Wallet 中符合条件的金融数据,包括美国的 Apple Card、Apple Cash、Savings,以及英国的已连接账户数据。FinanceKit 可在设备上访问账户、余额和交易记录,并由用户控制授权。支持 Swift 6.3 / 当前 Apple 平台;查询 API 从 iOS/iPadOS 17.4 起提供,TransactionPicker 从 iOS/iPadOS 18 起提供,后台投递从 iOS/iPadOS 26 起提供。

FinanceKit 指南应聚焦于金融数据访问、Wallet 订单存储与查询、TransactionPicker 和后台投递。Apple Pay 结账应使用 PassKit;Widget 界面与时间线工作应使用 WidgetKit;Wallet 订单追踪邮件或 Apple Business Connect 优化不属于此技能的范围。

目录

设置与权利

要求
  1. 受管理的权利 -- 通过 FinanceKit 权利申请表向 Apple 申请 com.apple.developer.financekit。这是一项受管理的功能;Apple 会审核每个应用。
  2. 组织级 Apple Developer 账户(个人账户不符合条件)。
  3. 申请此权利需要账户持有人角色。
  4. 符合条件的 App Store 应用 -- 应用必须属于 Finance 类别,通过 App Store 在美国或英国面向 iPhone 分发,并提供净资产、支出或预算等财务管理工具。
  5. 按 Bundle ID 审批 -- Apple 会将此权利分配给获批的 Bundle ID;不要假定它会自动适用于同一系列的其他应用或扩展。
  6. 如果应用直接或通过受监管的机构提供金融产品,则必须允许客户将这些账户连接到 Apple Wallet,并通过 FinanceKit 共享数据。
项目配置
  1. Apple 批准申请后,通过 Xcode 受管理的功能添加 FinanceKit 权利。
  2. 将 NSFinancialDataUsageDescription 添加到 Info.plist -- 此字符串会在授权提示中向用户显示。
  3. 对于 iOS 26 后台投递,将 FinanceKit 权利添加到应用和扩展目标,然后使用 App Groups 进行共享存储。
<key>NSFinancialDataUsageDescription</key>
<string>This app uses your financial data to track spending and provide budgeting insights.</string>

数据可用性

美国的 FinanceKit 金融数据要求 iOS/iPadOS 17.4+,目前涵盖符合条件的 Apple Card、Apple Cash 和 Savings 数据;Apple Card Family 参与者和 Apple Cash Family 子女不包含在内。英国支持要求 iOS/iPadOS 18.4+,并使用开放银行服务访问受支持机构的数据。订单 API 与金融数据查询 API 分开提供。

在进行任何 API 调用之前,请检查设备是否支持 FinanceKit。此值在不同启动和 iOS 版本之间保持不变。

import FinanceKit

guard FinanceStore.isDataAvailable(.financialData) else {
    // FinanceKit not available -- do not call any other financial data APIs.
    // The framework terminates the app if called when unavailable.
    return
}

对于 Wallet 订单:

guard FinanceStore.isDataAvailable(.orders) else { return }

数据可用性返回 true 并不保证设备上存在数据。数据访问也可能暂时受限(例如,Wallet 不可用、MDM 限制)。访问受限时会抛出 FinanceError.dataRestricted,而不会导致程序终止。

授权

请求授权以访问用户选择的金融账户。系统会显示账户选择器,用户可在其中选择要共享的账户以及可公开的最早交易日期。

let store = FinanceStore.shared

let status = try await store.requestAuthorization()
switch status {
case .authorized:    break  // Proceed with queries
case .denied:        break  // User declined
case .notDetermined: break  // No meaningful choice made
@unknown default:    break
}
检查当前状态

查询当前授权状态,不显示提示:

let currentStatus = try await store.authorizationStatus()

用户授予或拒绝访问权限后,requestAuthorization() 会返回缓存的决定,不会再次显示提示。用户可以在“设置”>“隐私与安全性”>“财务数据”中更改访问权限。

查询账户

账户建模为一个包含两个 case 的枚举:.asset(例如 Apple Cash、储蓄账户)和 .liability(例如 Apple Card 信用账户)。两者共享通用属性(id、displayName、institutionName、currencyCode),而负债账户还包含信用相关字段。

func fetchAccounts() async throws -> [Account] {
    let query = AccountQuery(
        sortDescriptors: [SortDescriptor(\Account.displayName)],
        predicate: nil,
        limit: nil,
        offset: nil
    )

    return try await store.accounts(query: query)
}
使用账户类型
switch account {
case .asset(let asset):
    print("Asset account, currency: \(asset.currencyCode)")
case .liability(let liability):
    if let limit = liability.creditInformation.creditLimit {
        print("Credit limit: \(limit.amount) \(limit.currencyCode)")
    }
}

账户余额

余额表示账户在某个时间点的金额。CurrentBalance 有三个 case:.available(包含待处理金额)、.booked(仅包含已入账金额)或 .availableAndBooked。

func fetchBalances(for accountID: UUID) async throws -> [AccountBalance] {
    let predicate = #Predicate<AccountBalance> { balance in
        balance.accountID == accountID
    }

    let query = AccountBalanceQuery(
        sortDescriptors: [SortDescriptor(\AccountBalance.id)],
        predicate: predicate,
        limit: nil,
        offset: nil
    )

    return try await store.accountBalances(query: query)
}
读取余额金额

金额始终为正数。使用 creditDebitIndicator 确定正负:

func formatBalance(_ balance: Balance) -> String {
    let sign = balance.creditDebitIndicator == .debit ? "-" : ""
    return "\(sign)\(balance.amount.amount) \(balance.amount.currencyCode)"
}

// Extract from CurrentBalance enum:
switch balance.currentBalance {
case .available(let bal):       formatBalance(bal)
case .booked(let bal):          formatBalance(bal)
case .availableAndBooked(let available, _): formatBalance(available)
@unknown default: "Unknown"
}

查询交易

使用 TransactionQuery 配合 Swift 谓词、排序描述符、限制和偏移量。

let predicate = #Predicate<Transaction> { $0.accountID == accountID }

let query = TransactionQuery(
    sortDescriptors: [SortDescriptor(\Transaction.transactionDate, order: .reverse)],
    predicate: predicate,
    limit: 50,
    offset: nil
)

let transactions = try await store.transactions(query: query)
读取交易数据
let amount = transaction.transactionAmount
let direction = transaction.creditDebitIndicator == .debit ? "spent" : "received"
print("\(transaction.transactionDescription): \(direction) \(amount.amount) \(amount.currencyCode)")
// merchantName, merchantCategoryCode, foreignCurrencyAmount are optional
内置谓词辅助方法

FinanceKit 提供了用于常见筛选条件的工厂方法:

// Filter by transaction status
let bookedOnly = TransactionQuery.predicate(forStatuses: [.booked])

// Filter by transaction type
let purchases = TransactionQuery.predicate(forTransactionTypes: [.pointOfSale, .directDebit])

// Filter by merchant category
let groceries = TransactionQuery.predicate(forMerchantCategoryCodes: [
    MerchantCategoryCode(rawValue: 5411)  // Grocery stores
])

如需查看交易字段表和更多查询模式,请阅读 references/financekit-patterns.md。

长时间运行的查询和历史记录

使用基于 AsyncSequence 的历史记录 API 进行追赶式同步、实时更新或可恢复同步。这些 API 会返回已插入、已更新和已删除条目的 ID,以及一个 HistoryToken。

func catchUpTransactions(for accountID: UUID) async throws {
    let history = store.transactionHistory(
        forAccountID: accountID,
        since: loadSavedToken(),
        isMonitoring: false  // finish after saved-token catch-up
    )

    for try await changes in history {
        removeLocalRecords(withIDs: changes.deleted)
        upsert(changes.inserted + changes.updated)
        saveToken(changes.newToken)
    }
}
持久化历史记录令牌

HistoryToken 遵循 Codable。将其持久化,以便恢复查询而无需重新处理数据:

func saveToken(_ token: FinanceStore.HistoryToken) {
    if let data = try? JSONEncoder().encode(token) {
        UserDefaults.standard.set(data, forKey: "financeHistoryToken")
    }
}

func loadSavedToken() -> FinanceStore.HistoryToken? {
    guard let data = UserDefaults.standard.data(forKey: "financeHistoryToken") else { return nil }
    return try? JSONDecoder().decode(FinanceStore.HistoryToken.self, from: data)
}

如果保存的令牌指向已压缩的历史记录,框架会抛出 FinanceError.historyTokenInvalid。丢弃该令牌,然后立即对受影响的账户或余额数据流运行一次全新的追赶式查询,以重建本地状态和替换令牌。仅对单独的实时监视器使用 isMonitoring: true。

账户和余额历史记录
let accountChanges = store.accountHistory(since: nil, isMonitoring: true)
let balanceChanges = store.accountBalanceHistory(forAccountID: accountID, since: nil, isMonitoring: true)

持续进行的预算同步应涵盖用户授权的数据模型:用于账户新增和移除的账户对象、用于趋势和小组件状态的账户余额,以及用于支出明细的交易。为每个数据流或账户使用单独的历史记录令牌,这样令牌压缩只会要求重新同步受影响的数据流。

交易选择器

对于需要选择性、临时访问而无需完整授权的应用,请使用 FinanceKitUI 中的 TransactionPicker。访问权限不会持久化——交易会直接传递,以供立即使用。

import FinanceKitUI

struct ExpenseImportView: View {
    @State private var selectedTransactions: [Transaction] = []

    var body: some View {
        if FinanceStore.isDataAvailable(.financialData) {
            TransactionPicker(selection: $selectedTransactions) {
                Label("Import Transactions", systemImage: "creditcard")
            }
        }
    }
}

Wallet 订单

FinanceKit 支持保存和查询 Wallet 订单(例如购买收据、配送跟踪)。

保存订单
let result = try await store.saveOrder(signedArchive: archiveData)
switch result {
case .added:        break  // Saved
case .cancelled:    break  // User cancelled
case .newerExisting: break // Newer version already in Wallet
@unknown default:   break
}
检查是否已有订单
let orderID = FullyQualifiedOrderIdentifier(
    orderTypeIdentifier: "com.merchant.order",
    orderIdentifier: "ORDER-123"
)
let result = try await store.containsOrder(matching: orderID, updatedDate: lastKnownDate)
// result: .exists, .newerExists, .olderExists, or .notFound
“添加到 Wallet”按钮(FinanceKitUI)
import FinanceKitUI

AddOrderToWalletButton(signedArchive: orderData) { result in
    // result: .success(SaveOrderResult) or .failure(Error)
}

后台传送

iOS 26+ 支持后台传送扩展,可在应用生命周期之外通知应用财务数据变更。在主应用中获得的用户授权会继承到扩展。两个目标都需要 FinanceKit 权限;使用 App Groups 在应用、扩展和相关小组件之间共享数据。

启用后台传送

这些注册方法是同步且不会抛出错误的;不要使用 try 或 await。

store.enableBackgroundDelivery(
    for: [.accounts, .accountBalances, .transactions],
    frequency: .daily
)

可用频率:.hourly、.daily、.weekly。这些频率表示数据变更时扩展启动之间预期的最短间隔;频率越低,扩展获得的处理窗口越大。

选择性或完全禁用:

store.disableBackgroundDelivery(for: [.transactions])
store.disableAllBackgroundDelivery()
后台传送扩展

在 Xcode 中创建后台传送扩展目标(Background Delivery Extension 模板)。直接在扩展类型上实现两个异步入口点,并且仅在保存好必要工作后,才从 didReceiveData(for:) 返回。

import FinanceKit

@main
struct MyFinanceExtension: BackgroundDeliveryExtension {
    func didReceiveData(for types: [FinanceStore.BackgroundDataType]) async {
        if types.contains(.transactions) {
            await processNewTransactions()
        }
        if types.contains(.accountBalances) {
            await updateBalanceCache()
        }
        if types.contains(.accounts) {
            await refreshAccountList()
        }
    }

    func willTerminate() async { await savePartialWork() }
}

常见错误

1. 数据不可用时调用 API

不要这样做——跳过可用性检查:

let store = FinanceStore.shared
let status = try await store.requestAuthorization() // Terminates if unavailable

应该这样做——先检查可用性:

guard FinanceStore.isDataAvailable(.financialData) else {
    showUnavailableMessage()
    return
}
let status = try await FinanceStore.shared.requestAuthorization()
2. 忽略借记/贷记指示符

不要这样做——将金额视为带符号的值:

let spent = transaction.transactionAmount.amount // Always positive

应该这样做——应用指示符:

let amount = transaction.transactionAmount.amount
let signed = transaction.creditDebitIndicator == .debit ? -amount : amount
3. 未处理数据限制错误

不要这样做——假定授权会持续有效:

let transactions = try await store.transactions(query: query) // Fails if Wallet restricted

应该这样做——捕获 FinanceError:

do {
    let transactions = try await store.transactions(query: query)
} catch let error as FinanceError {
    if case .dataRestricted = error { showDataRestrictedMessage() }
}
4. 用快照替代可恢复的历史记录

使用上文规范的交易历史循环,并仅在本地删除和更新操作提交后,才持久化每个 newToken。为每个数据流/账户分别保存令牌;遇到无效令牌错误时,仅重新同步受影响的范围。

5. 误解负债账户上的借记/贷记

资产账户和负债账户都使用 .debit 表示资金流出。但 .credit 的含义不同:在资产账户上表示收到资金;在负债账户上表示还款或退款,使可用信用额度增加。完整的解读表请参阅 references/financekit-patterns.md。

检查清单

  • 在任何 API 调用前检查 FinanceStore.isDataAvailable(.financialData)
  • 检查应用资格:Finance 类别、面向美国或英国用户通过 App Store 分发的 iPhone 应用、财务管理功能集、组织账户、Account Holder 申请
  • 为应用的 bundle ID 申请并获批 com.apple.developer.financekit 权限
  • 在 Info.plist 中设置 NSFinancialDataUsageDescription,并填写清晰、具体的说明
  • 处理所有授权状态(.authorized、.denied、.notDetermined)
  • 捕获并妥善处理 FinanceError.dataRestricted
  • 正确应用 CreditDebitIndicator 处理金额(而不是将其视为带符号的值)
  • 持久化历史记录令牌,以支持可恢复查询
  • 遇到 FinanceError.historyTokenInvalid 时,丢弃令牌并立即重新同步受影响的数据流
  • 持续同步计划涵盖已授权的账户、余额和交易,而不只是交易
  • 不需要实时更新时,长时间运行的查询使用 isMonitoring: false
  • 不需要完整授权时,使用交易选择器
  • 只查询应用确实需要的数据
  • 明确从本地账户、余额或交易存储中删除历史记录变更返回的 ID
  • 后台传输调用使用同步的 iOS 26 API,并确保扩展与主应用位于同一 App Group 中
  • 后台传输注册所有需要的数据类型:.accounts、.accountBalances 和/或 .transactions
  • 同时为应用和后台传输扩展目标添加 FinanceKit 权限
  • 用户撤销访问权限时删除财务数据

参考资料

评论

支持 Markdown