☰
square_connect鸿蒙适配实战:Flutter支付插件迁移HarmonyOS全方案
2026/9/29 3:41:21 网站建设 项目流程

1. 项目概述:为什么偏偏是 square_connect?

做 Flutter 支付相关开发的朋友,对 square_connect 这个名字应该不陌生。它是 Square 官方维护的 Flutter 支付插件,主要面向欧美市场,支持信用卡刷卡、Apple Pay、Google Pay 这些主流支付方式。Square 的设备生态很完整,从 Reader S2 到 Stand,都能通过这个插件直接驱动,这也是很多出海应用选择它的原因。

但问题来了——如果你现在做的是鸿蒙应用的支付模块,square_connect 几乎没法直接用。原因很简单:鸿蒙 NEXT 不再兼容 Android 的 Framework API,而 square_connect 的底层实现大量依赖 Android 的 Activity、Intent、BroadcastReceiver 等机制。你没办法把一个跑在 Android 上的 Flutter 插件,不加修改就搬到鸿蒙上。强行编译,CardEntryActivity 找不到,PaymentIntent 的 intent 跳转逻辑也全部失效。

这篇内容就是把我在实际项目中做的 square_connect 鸿蒙化适配过程完整拆一遍。不是原理空谈,而是踩过坑之后的实操记录,涉及 MethodChannel 的替换、ArkTS 侧的原生实现、支付中台的架构设计,以及一系列编译期和运行期的调优。适配完成后,同一套 Flutter 业务代码可以做到 Android/iOS/鸿蒙三端跑通,这也是鸿蒙应用要做支付中台时的一条可落地路径。

适用对象很明确:已经在用 Flutter 做跨端应用、现在需要兼容鸿蒙的团队,或者正在评估鸿蒙支付方案、想知道 square_connect 能不能救一把的开发者。如果你对鸿蒙 Flutter 生态完全没接触过,也没关系,下文会从工程结构讲起,关键是带着你把整条链路走通。

2. 适配前必须想清楚的架构问题

2.1 square_connect 的底层到底依赖了什么

要适配一个三方库,第一步不是看它的 Dart 接口,而是把它的原生依赖链彻底摸清楚。square_connect 的 Android 端实现,核心依赖大概可以归纳为三块:

第一块是 Activity 跳转体系。Square 的支付流程不是全部在 Flutter 层完成的,比如读卡器连接、卡信息输入、支付确认这些界面,都是通过启动原生的 Activity 来做的。Flutter 侧调用 startPaymentIntent,本质上是把一个 PayIntent 对象传给原生,原生再 startActivity 跳到 Square 的支付界面。这套机制在 Android 上很成熟,但在鸿蒙上,Activity 这个概念已经不存在了,取而代之的是 UIAbility、ServiceExtension 和 Want 跳转。所以适配的时候,这一层必须完全重写。

第二块是蓝牙和硬件通信。Square 读卡器通过蓝牙与手机连接,底层用的是 Android 的 BluetoothAdapter 接口。鸿蒙有自己的 BluetoothManager,API 完全不一样。如果你只想适配纯软件支付(比如手动输入卡号、Apple Pay 这类),蓝牙硬件这块可以暂时砍掉;但如果你要接读卡器,就没有捷径可走了,只能基于鸿蒙的蓝牙API重新实现设备发现、连接、通信协议。

第三块是网络层和签名验证。Square 的支付 API 要求每次请求都要带 HMAC 签名,签名规则是基于你的应用 ID 和密钥对请求体做哈希。这一层实际上不依赖 Android 特有的 API,属于纯 Java/ArkTS 都能实现的逻辑,所以适配的重点不在这,但要注意你在 Android 侧用的那个签名工具类,在鸿蒙侧要能找到对应的等价实现。

明确了这三块依赖,适配的策略就清楚了:能不碰硬件的先不碰,能把逻辑层挪到 Dart 层的尽量挪,真正必须写在原生侧的,用鸿蒙的 API 重写一遍。

2.2 适配策略的选择:全量重写还是兼容层

既然不兼容,摆在面前的路其实有三条,我分别说一下利弊。

第一条是全量重写:不依赖 square_connect,直接在鸿蒙工程里调鸿蒙的支付API,比如接入鸿蒙自家的 IAP 或者第三方支付 SDK。这条路最干净,但问题是 Flutter 侧的业务代码也要跟着改,而且如果老板要求三端一致,那 Android 和 iOS 的支付逻辑也得回来重写,工程量翻倍。

第二条是兼容层适配:保留 square_connect 的 Dart 接口不变,但在鸿蒙工程里写一套新的原生端实现,把原 Dart API 调用映射到鸿蒙的支付能力上。这样 Flutter 业务层的代码完全不用动,只换底层的原生实现。这是我在项目里走的路,也是这篇指南的核心思路。

第三条是 Channel 拦截:在 Flutter 和原生之间插一层代理,拦截 square_connect 发出来的所有 MethodCall,转发给鸿蒙侧的自定义实现。这个方案的好处是连插件的注册逻辑都可以劫持,理论上改动量最小,但坏处是调试困难,而且 Square 插件内部如果用了 PlatformView 或者自定义纹理这类机制,拦截层不一定兜得住。

对比一下这三条路:

方案Flutter侧改动原生侧工作量维护成本推荐指数
全量重写大大中两颗星
兼容层适配无中中四颗星
Channel拦截小中高三颗星

最终确定兼容层这个方案,是因为 square_connect 的 Dart 接口设计得比较稳定,而且支付这个场景的核心链路(发起支付、等待回调、返回结果)本质上就是一个异步结果,不涉及复杂的生命周期传递。这个特性决定了它很适合做接口兼容。

2.3 鸿蒙 Flutter 工程的插件注册机制变化

做兼容层适配之前,还有一个基础功课必须补:鸿蒙版的 Flutter 插件是怎么注册的。

在 Android 上,Flutter 插件通过 FlutterPlugin 接口注册,插件类会被自动发现,然后在 onAttachedToEngine 里拿到 MethodChannel 并设置 Handler。鸿蒙这边的机制大差不差,但有几个关键差异:

第一,鸿蒙的 Flutter 插件需要单独创建 extension 类型的工程模块,因为 Flutter 引擎在鸿蒙上跑在一个特定的 Ability 里,插件的原生端实际上是一个 ServiceExtension,通过 ExtensionConnection 和 Flutter 引擎通信。

第二,插件注册不是用清单文件自动发现,而是在 Flutter 引擎初始化的时候手动显式注册。也就是说,你打开entry/src/main/ets/entryability/EntryAbility.ets,里面有一段代码是调用flutterEngine.registerPlugin来注入插件的。

第三,MethodChannel 的 native 端在鸿蒙里不是一个现成的 FlutterPlugin 接口,而是通过FlutterPluginBinding来注册相关回调。具体写法是binding.getBinaryMessenger().registerChannel(name, handler)。

这三个差异决定了你的原生适配文件应该放在工程的哪个位置、注册入口在哪里。我最初在这个问题上卡了很久,一直没找到插件注册的主动权在哪,最后翻了鸿蒙 Flutter SDK 的源码才发现,它把 Android 的自动注册改成显式注册了。这个设计对二进制发布更友好,但对开发者来说,多了一步手动配置。

3. 鸿蒙化适配的核心实施过程

3.1 工程改造的前置准备

我接手这个项目的时候,工程结构是标准的三端 Flutter 项目,plugin 是以 package 形式引入的square_connect: ^2.0.0。要开始鸿蒙适配,第一步要做的是在工程里添加鸿蒙模块支持。

具体操作分三步走。

第一步,升级 Flutter 到支持 OpenHarmony 的版本。这里要注意,不是随便一个 Flutter 版本都能跑鸿蒙,你需要使用 OpenHarmony 分叉的 Flutter 引擎,目前比较稳定的是从 Flutter 3.7 分叉的 ohos 分支。切换之后,flutter doctor会多出一个 OpenHarmony 的 toolchain 检测项,能识别到 DC 的 SDK 路径,才说明环境OK了。

第二步,创建鸿蒙插件模块。用 DevEco Studio 创建一个SquareConnectPlugin的 Module,类型选择Flutter Plugin,语言选 ArkTS。创建完成后,这个 Module 下面会有ohos目录,里面是原生代码的存放位置。

第三步,在entry模块的EntryAbility里手动注册插件。具体代码是:

import { FlutterEngine } from '@ohos/flutter_ohos'; import { SquareConnectFlutterPlugin } from '@ohos/square_connect_plugin'; let engine = new FlutterEngine(this.context); engine.registerPlugin(new SquareConnectFlutterPlugin(binding)); engine.load();

注册的位置有讲究:一定要在engine.load()之前完成,否则插件初始化时会拿不到 engine 的 binaryMessenger。我在第一次跑的时候把注册放到了 load 之后,结果控制台一直在报not register channel的错,排查了半天才发现是顺序问题。

还有一个容易踩的坑:鸿蒙工程里同时会有两个 Module,一个是entry(应用入口),一个是插件 Module。注册插件的代码写在 entry 里,但插件 Module 自身的Index.ets导出要和 package 名对上。如果对不上,编译能过,但运行时会报Cannot find module错误。

3.2 MethodChannel 替换:从 Android 实现到鸿蒙实现

square_connect 在 Flutter 侧用了一个 MethodChannel,名字叫square_connect,所有支付指令都通过invokeMethod走这个 Channel。Android 原生端的实现类入口是SquareConnectPlugin.kt,我的任务就是在鸿蒙侧写一个同名 Channel 的 Handler,把这些方法一个个接住。

先看 Flutter 侧调用了哪些方法。以核心的支付流程为例,Dart 代码基本长这样:

import 'package:square_connect/square_connect.dart'; final paymentIntent = PayIntent( amountMoney: Money(amount: 1000, currencyCode: 'USD'), paymentMethod: PaymentMethod.CARD, ); final result = await SquareConnect.instance.startPaymentIntent(paymentIntent);

底层实际上调用了channel.invokeMethod('startPaymentIntent', intent.toMap())这一条通道。我在鸿蒙侧要做的就是注册square_connect这个 Channel,监听startPaymentIntent,然后把参数解析成鸿蒙侧内部的数据结构,调用鸿蒙支付服务,最后把结果回调回去。

鸿蒙侧核心实现大概是这样的框架:

import { BinaryMessenger, MethodCall, MethodChannel } from '@ohos/flutter_ohos'; export class SquareConnectPlugin { private channel: MethodChannel; constructor(binding: FlutterPluginBinding) { this.channel = binding.getBinaryMessenger() .registerChannel('square_connect', (call: MethodCall) => { return this.handleMethodCall(call); }); } private async handleMethodCall(call: MethodCall): Promise<any> { switch (call.method) { case 'startPaymentIntent': return this.startPaymentIntent(call.arguments); case 'isPlatformSupported': return true; case 'getSquareSettings': return this.getSquareSettings(); default: throw new Error('Unknown method: ' + call.method); } } }

这里有个很关键的差异:Android 侧MethodChannel的setMethodCallHandler是同步接口,handler 直接返回结果或者抛出异常;但鸿蒙侧的registerChannel是支持 Promise 的,也就是说你可以直接返回一个异步操作的 Promise,Flutter 侧的invokeMethod会等这个 Promise resolve 之后再拿到结果。这个特性太重要了,因为支付本来就是一个异步过程,如果鸿蒙侧不支持 Promise,我还得自己实现回调队列。

3.3 支付流程的鸿蒙化落地:授权、下单、支付、回调

square_connect 的完整支付流程可以分为四个阶段:授权、下单、支付、回调。每个阶段在鸿蒙侧都需要处理不同的细节。

授权阶段,Android 上是通过SquareConnect.getInstance().requestAccessToken()弹出一个 OAuth 网页让用户授权。鸿蒙侧我用了 WebView 组件加载授权页,然后拦截 URL 里的 code 参数。这个思路是通的,因为 Square 的 OAuth 流程是基于浏览器重定向的,跟平台关联不大。

下单阶段,主要是调用 Square API 创建 PaymentIntent。这一层是网络请求,在鸿蒙侧直接用@ohos.net.http的createHttpClient发 POST 请求就行。需要注意的两点:一是请求头里要带Square-Version和Authorization: Bearer,这两个字段不能丢;二是鸿蒙的 HTTP 客户端默认不开启 TLS 1.3,而 Square API 强制要求 TLS 1.3,所以要在HttpRequestOptions里显式配置usingProtocol: TLS。

支付阶段,这是最复杂的一环。如果你是走读卡器支付,必须用鸿蒙的 BluetoothManager 重写设备连接逻辑。我第一版适配直接砍掉了读卡器,只做手动输入卡号 + Apple Pay 走系统支付的场景。Apple Pay 在鸿蒙上不存在,但鸿蒙有自己的钱包服务,可以调用pay.getWalletPayService()来拉起系统支付面板。这里我没法给出太详细的代码,因为最终方案依赖你对接的支付渠道,但大思路是一致的:拿到支付令牌之后,用鸿蒙的钱包服务完成扣款,然后返回签名后的结果。

回调阶段,square_connect 的 Android 实现是通过 BroadcastReceiver 接收支付结果的,鸿蒙侧没有这个机制。我改用状态轮询:支付发起后,前端轮询查询支付状态接口,拿到终态之后更新 UI。虽然不如推送实时,但胜在实现简单稳定,不需要维护长连接。

3.4 权限与签名:鸿蒙 module.json5 的配置细节

鸿蒙的权限声明是在entry/src/main/module.json5里配置的,和 Android 的AndroidManifest.xml是一个作用。我做适配时主要加了以下权限:

{ module: { requestPermissions: [ { name: "ohos.permission.INTERNET" }, { name: "ohos.permission.BLUETOOTH" }, { name: "ohos.permission.GET_NETWORK_INFO" } ] } }

这里值得单独说的是ohos.permission.BLUETOOTH。鸿蒙的蓝牙权限是分层的,普通蓝牙通信和扫描设备用的权限不同,如果你在后面接了读卡器设备,还需要加ohos.permission.BLUETOOTH_SCAN。但注意,这两者不是同一级别,一个应用如果同时要跑服务端和客户端模式,需要分别申请。

还有一点容易忽略:鸿蒙应用在请求网络权限之外,如果你的支付请求要访问 HTTPS 接口,还需要在应用签名时申请Domain级别的网络权限验证。也就是说,你的应用必须通过华为的签名服务,把证书链校验搞定,否则在鸿蒙的严格网络环境下,请求会被直接拦截。这个机制类似 iOS 的 ATS,但不完全一样,鸿蒙是在系统层做证书固定,所以调试期内建议先在module.json5里临时把 dev 环境的校验关掉,上线前再打开。

提示:开发阶段如果频繁遇到Http request failed with code: 200 but TLS handshake failed,十有八九是 TLS 版本不对或者证书链没配置好。先把 HTTP 协议的版本确认清楚,再查签名,不要一上来就怀疑业务代码。

4. 把支付能力做成中台:从插件适配到架构升级

4.1 统一支付抽象层:AllPayFacade

适配完 square_connect 之后,我发现一个问题:如果只是把 Channel 通了,那你的工程仍然绑定在 Square 这一个支付渠道上。一旦未来要接别的支付,或者 Square 本身的服务在某个区域不可用,你的业务代码又要跟着改。所以我在适配的基础上做了一层抽象,把支付能力沉淀成中台结构。

这个抽象层我用一个单例类来承载,叫AllPayFacade。它对外暴露的接口和具体支付渠道无关,只包含支付相关的原子能力:

abstract class PaymentGateway { Future<PaymentResult> pay(PaymentRequest request); Future<bool> isAvailable(); Future<String> getDeviceInfo(); Future<List<PaymentMethod>> getSupportedMethods(); }

然后为 square_connect 写一个SquarePaymentGateway,实现这个接口,内部调用改造后的鸿蒙兼容层。如果以后接 IAP,只需要再写一个IapPaymentGateway,业务侧调用方完全不感知底层变化。

这套抽象的好处我在项目里体会特别深。当时业务方提了一个需求:希望小额支付走 QuickPay,大额支付走完整刷卡流程。如果代码直接散在业务逻辑里,这个需求要改很多地方;但有了 Facade 之后,我只需要在 Facade 内部做一个金额路由判断,对业务层暴露的还是同一个接口。

4.2 订单服务与支付状态机

支付中台和普通的支付封装最大的区别,在于它一定要有订单状态管理。我参考了业界通用的支付状态流转,设计了一个状态机:

状态说明可流转目标
INIT订单创建,支付未开始PENDING, CANCELLED, CLOSED
PENDING支付进行中(用户等待结果)SUCCESS, FAILED, TIMEOUT
SUCCESS支付成功REFUNDING, REFUNDED
FAILED支付失败PENDING, CLOSED
TIMEOUT超时未收到回调PENDING, CLOSED
CLOSED订单终态,不可再操作无

这个状态机是整个支付中台的骨架。为什么需要它?因为真实场景里,支付结果不是即时返回的。用户可能发起支付之后切走应用,过五分钟回来,你要能查到这个单子现在是 PENDING 还是 SUCCESS。如果在 PENDING 状态就继续轮询,在 SUCCESS 状态就要触发后续的发货逻辑。没有状态机,这部分逻辑每接入一个渠道都要重写一遍。

另外还要提一个细节:状态机的判断要放在中台层,不要放在 UI 层。我在第一版里把支付成功判断写在了一个页面的 setState 里,后来业务加了一个从通知栏唤起的场景,直接从支付结果跳进订单页,发现状态对不上,排查很久才发现是 UI 层的状态没同步到中台。移到中台层后,UI 只负责展示,状态流转由中台统一推动。

4.3 支付回调的幂等处理与对账

支付回调是中台最容易出 bug 的地方。Square 的回调机制是向你的服务端发 Webhook,然后服务端再推送结果给客户端。这里有两个问题必须处理:回调可能重复,回调可能丢失。

重复的问题靠幂等表解决。服务端收到每个事件时,先查一下支付单号是否已经处理过,如果处理过直接返回成功,不再执行后续逻辑。这个表不用建得很复杂,一个order_id + event_type + created_at的联合唯一键就够了。

丢失的问题要靠主动对账。设计一个定时任务,每隔一定周期扫描一次 PENDING 状态的订单,向 Square 发起一次查询支付状态的请求,把终态同步回来。对账周期我用的十五分钟,因为支付渠道的查询接口响应很慢,十五分钟是一个实测比较平衡的值,再短渠道会有压力,再长用户可能等不及。

提示:千万别小看对账这步。我遇到过一种情况:Square 返回支付成功,但客户端的网络断了,Webhook 没送到服务端,用户看到的是支付失败,实际钱已经扣了。如果没有对账机制,这个单子的钱就悬空了。加了主动对账之后,这种不一致会在一刻钟内自动被发现并修复。

4.4 多支付渠道的降级策略

中台化之后,还有一个必须考虑的能力:渠道降级。

Square 再好,也可能有服务不可用的时候。我在适配中台时设计了一个简单的降级判断:每次发起支付之前,先调用isAvailable()检查当前渠道健康状态,如果连续三次调用返回 false,就自动把流量切换到备选渠道。这个降级策略在代码里就是一行简单的计数器判断,但它的价值在于让支付模块具备了一定的自愈能力。

还有一个实际场景:Square 在部分国家/地区不可用。如果应用的数据显示用户所在区域不支持 Square,那中台应该从一开始就只用 IAP 或其他本地支付,而不是让用户走到支付环节才发现不可用。中台层可以根据设备区域设置,在初始化时指定默认渠道,这就是“归因”逻辑。

5. 常见问题与排障实录

5.1 编译期问题:插件未注册与导入路径错误

编译期的问题,我遇到最多的是两类。

第一类是Cannot find module。这个问题的根源通常是 Module 的Index.ets导出名和注册时引用的包名不一致。DevEco Studio 创建插件 Module 的时候会自动生成一个 Index.ets,里面导出了插件类,但是类名默认是根据 Module 名生成的,需要手动检查一下是否和你EntryAbility里registerPlugin传的类一致。

第二类是Flutter Engine is not initialized when plugin registered。这个错在 Android 上很少出现,因为 Android 的插件注册时机由框架管理。鸿蒙改成手动注册后,你必须保证注册发生在engine.load()之前。查看你的EntryAbility代码,确认注册行号在 load 前面。

5.2 运行期问题:Channel 不响应与方法找不到

跑起来之后,最典型的错误是 Flutter 侧调用invokeMethod之后一直卡在 Future 上不返回。这个现象大概率是鸿蒙侧的 Channel 注册名和 Flutter 侧不一致。

我遇到过的情况是,square_connect 的 Dart 端用的是square_connect这个字符串作为 Channel 名,但鸿蒙侧我创建 Channel 的时候不小心用了square_connect_method,结果两边对不上,所有调用都石沉大海。排查方法很简单:在鸿蒙侧 handler 的第一行加一个hilog打个日志,如果 Flutter 侧真的发起了调用,这里一定会打印日志。如果没有日志,说明 Channel 名对不上或者注册时机有问题。

第二种常见情况是Unknown method。这是 Channel 通了,但方法名对不上。打开 square_connect 的源码,把 Dart 侧所有invokeMethod的字符串都列出来,挨个在自己的 Handler 里补上对应的 case。记住,宁可多实现几个空方法,也不要漏掉一个,否则线上会在你意想不到的路径上报错。

5.3 网络问题:TLS 握手失败与证书校验

Square API 的域名是api.squareup.com,这是一个强制 TLS 1.3 的接口。鸿蒙的 HTTP 客户端默认可能走 TLS 1.2,我第一次请求的时候直接遇到了握手失败。

解决方法是显式指定 TLS 版本,在HttpRequestOptions里加上:

let httpRequest = http.createHttp(); let response = await httpRequest.request('https://api.squareup.com/v2/payments', { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, usingProtocol: http.HttpProtocol.HTTP1_1_TLS13, extraData: JSON.stringify(body) });

另外,如果测试时发现请求被系统 CA 校验拦住了,可以先临时把module.json5里的网络安全配置放开,上线前再恢复强制校验。

5.4 设备适配:PENDING 状态卡死的兜底方案

最后一个想提的,是 PENDING 状态卡死的问题。在 Android 的 square_connect 里,支付回调是实时推的,基本不存在长时间 PENDING。但在鸿蒙侧我改成了轮询之后,出现了极端情况:支付已经成功,但轮询接口恰好返回了一次网络异常,导致状态一直卡在 PENDING。

我的兜底方案是加一个超时看门狗:支付状态进入 PENDING 之后,启动一个倒计时,比如三分钟。如果三分钟内没有变成终态,就强制将订单标记为 TIMEOUT,并提示用户联系客服或者手动刷新。这里要注意,标记为 TIMEOUT 不等于这笔交易失败,你还需要在对账阶段把这个单子的真实状态查回来,如果发现实际扣款了,要走自动退款流程。

6. 写在最后的几点实操体会

整个适配过程做下来,我最想分享的一点是:不要试图一步到位把所有功能都搬过来。最初我的计划是把 square_connect 的所有方法全部在鸿蒙侧复刻一遍,结果工作量巨大,而且很多方法你根本用不到。更务实的做法是先梳理自己业务里实际调用了哪些 API,只适配这一部分,跑通主流程之后,再按需补齐边缘功能。

还有个小技巧:把 square_connect 的源码里所有invokeMethod的位置列出来,对照官方文档里每个方法的功能标记为“核心链路 / 可选链路 / 已废弃”,这个清单就是你鸿蒙适配的施工图。我在项目里就是靠这张图,把两周的适配工作压缩到了五天,因为第三周的时间省在了不做无用功上。

续扩展的方向也挺明确:如果你后续要支持读卡器设备,可以基于鸿蒙的 BluetoothManager 单独做一个阅读器服务模块,把设备连接层从支付逻辑里剥离开。卡在蓝牙这一层其实不用慌,鸿蒙的蓝牙API设计得比 Android 更简洁,重点是处理好设备回调的生命周期。整体来说,square_connect 的鸿蒙化适配不是一个不可解的题,只要把 Channel 换掉、把接口补全、把状态理清,你的 Flutter 应用距离真正跑在鸿蒙上,就只差这一步了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询