PAYATHON 2026

Siri IntentHandler 中 LocalAuthentication 无法正常工作

支付小周

结论

LocalAuthentication 本身没有问题。真正的问题是 handleSendPayment:completion: 的异步流程没有调用 completion。

创建 INSendPaymentIntentResponse 只会得到一个响应对象,不会自动把结果交给 Siri。无论 Touch ID 成功、失败、不可用,还是支付失败,处理结束后都必须调用 completion,而且只能调用一次:

completion(response);

Siri 只有收到 INSendPaymentIntentResponseCodeSuccess,才会展示“付款已发送”的成功界面。

为什么当前代码没有响应

原代码有以下问题:

  1. siriResponse 赋值后没有传给 completion。
  2. Touch ID 成功后只执行了支付代码,没有返回最终结果。
  3. canEvaluatePolicy:error: 返回 NO 时,方法直接结束,Siri 收不到任何响应。
  4. Touch ID 失败时却创建了 INSendPaymentIntentResponseCodeSuccess,状态与实际结果不符。
  5. evaluatePolicy:localizedReason:reply: 是异步调用。Intent Extension 必须等异步任务完成后再结束响应流程。
  6. 支付逻辑通常不需要在主线程执行。只有明确依赖主线程的代码才需要切换到主队列。

推荐实现

把 Touch ID 验证、支付处理和 Siri 回调串成完整的异步流程,确保每条路径都会调用 completion。

#import <Intents/Intents.h>
#import <LocalAuthentication/LocalAuthentication.h>

- (void)handleSendPayment:(INSendPaymentIntent *)intent
               completion:(void (^)(INSendPaymentIntentResponse *response))completion {

    LAContext *context = [[LAContext alloc] init];
    NSError *authenticationError = nil;

    BOOL canAuthenticate =
        [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
                             error:&authenticationError];

    if (!canAuthenticate) {
        INSendPaymentIntentResponse *response =
            [[INSendPaymentIntentResponse alloc]
                initWithCode:INSendPaymentIntentResponseCodeFailureRequiringAppLaunch
                userActivity:[self paymentUserActivityForIntent:intent]];

        completion(response);
        return;
    }

    [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
            localizedReason:@"使用 Touch ID 确认付款"
                      reply:^(BOOL success, NSError *error) {

        if (!success) {
            INSendPaymentIntentResponseCode code =
                INSendPaymentIntentResponseCodeFailure;

            /*
             * 如果需要让用户进入主 App 重试,或者当前扩展环境无法显示
             * 生物识别界面,可以改用 FailureRequiringAppLaunch。
             */
            if (error.code == LAErrorNotInteractive) {
                code = INSendPaymentIntentResponseCodeFailureRequiringAppLaunch;
            }

            INSendPaymentIntentResponse *response =
                [[INSendPaymentIntentResponse alloc]
                    initWithCode:code
                    userActivity:(code == INSendPaymentIntentResponseCodeFailureRequiringAppLaunch
                                      ? [self paymentUserActivityForIntent:intent]
                                      : nil)];

            completion(response);
            return;
        }

        [self sendPaymentForIntent:intent
                        completion:^(BOOL paymentSucceeded, NSError *paymentError) {

            INSendPaymentIntentResponseCode code =
                paymentSucceeded
                    ? INSendPaymentIntentResponseCodeSuccess
                    : INSendPaymentIntentResponseCodeFailure;

            INSendPaymentIntentResponse *response =
                [[INSendPaymentIntentResponse alloc]
                    initWithCode:code
                    userActivity:nil];

            completion(response);
        }];
    }];
}

辅助方法需要根据应用的实际支付流程实现:

- (void)sendPaymentForIntent:(INSendPaymentIntent *)intent
                  completion:(void (^)(BOOL succeeded, NSError *error))completion {

    // 校验收款人、金额、账户状态,并异步调用支付服务。
    // 支付服务返回后调用 completion,不能遗漏任何失败路径。

    [self.paymentService sendPaymentWithIntent:intent
                                    completion:^(NSError *error) {
        completion(error == nil, error);
    }];
}

- (NSUserActivity *)paymentUserActivityForIntent:(INSendPaymentIntent *)intent {
    NSUserActivity *activity =
        [[NSUserActivity alloc] initWithActivityType:@"com.example.payment.authentication"];

    activity.title = @"确认付款";
    activity.userInfo = @{
        @"intentIdentifier": intent.identifier ?: @""
    };

    return activity;
}

是否需要切换到主线程

不必为了调用 completion 强制切换到主线程。LocalAuthentication 的 reply block 不保证在主线程执行,但 Siri Intent 的 completion handler 通常可以直接从当前回调队列调用。

如果支付 SDK 明确要求从主线程调用,只需把对应操作放进主队列:

dispatch_async(dispatch_get_main_queue(), ^{
    [self startPaymentWithCompletion:^(BOOL succeeded, NSError *error) {
        INSendPaymentIntentResponse *response =
            [[INSendPaymentIntentResponse alloc]
                initWithCode:(succeeded
                                  ? INSendPaymentIntentResponseCodeSuccess
                                  : INSendPaymentIntentResponseCodeFailure)
                userActivity:nil];

        completion(response);
    }];
});

dispatch_async 不会同步执行。如果业务需要等待支付结果,就必须在支付完成的回调中向 Siri 返回响应,不能在提交异步任务后立即结束方法。

生物识别无法在扩展中显示时

Intent Extension 的运行时间和界面展示能力有限。在某些系统版本、设备状态或扩展运行环境下,evaluatePolicy: 可能返回 LAErrorNotInteractive。遇到这种情况,不应继续静默等待。

可以按以下方式处理:

  1. 返回 INSendPaymentIntentResponseCodeFailureRequiringAppLaunch。
  2. 附带 NSUserActivity。
  3. 让用户进入主 App。
  4. 在主 App 前台完成 Touch ID 或 Face ID 验证。
  5. 验证成功后再提交支付。

不要先在 Siri 中返回成功,再打开 App 验证。Success 只能表示付款已经真正完成。

还需要注意的细节

  • completion 必须调用一次,既不能遗漏,也不能重复。
  • Touch ID 取消、锁定、验证失败或系统取消都不能当作支付成功处理。
  • 只有支付成功后才能返回 INSendPaymentIntentResponseCodeSuccess,否则 Siri 可能显示错误的最终状态。
  • Intent Extension 有执行时间限制,不适合长时间等待支付服务。如果操作无法及时完成,应考虑返回需要打开 App 的响应。
  • LAPolicyDeviceOwnerAuthenticationWithBiometrics 只允许生物识别。如果业务允许使用设备密码作为后备验证,可以评估 LAPolicyDeviceOwnerAuthentication,具体取决于应用的安全策略和目标系统版本。
  • 应保留并检查 LAError,特别是 LAErrorUserCancel、LAErrorBiometryLockout 和 LAErrorNotInteractive。
  • 如果其他方法还需要取消或管理 LAContext,可以将其保存为 IntentHandler 的强引用属性。不要用临时局部状态记录尚未完成的支付结果。

原代码最直接的修复方式,是在支付真正完成后创建响应并调用:

INSendPaymentIntentResponse *response =
    [[INSendPaymentIntentResponse alloc]
        initWithCode:INSendPaymentIntentResponseCodeSuccess
        userActivity:nil];

completion(response);

要完整解决问题,还需要覆盖生物识别不可用、验证失败、支付失败和需要打开主 App 的情况。

备注:内容仅供参考。