【Flutter x Square】Square APIで学ぶMethodChannel
参考
- Square SDK一覧 : https://developer.squareup.com/reference/square
- Payments:https://developer.squareup.com/docs/payments
Square Payment関連のAPI・SDK
SquareのAPI・SDKの構成
- In-App Payment
- In-Person
- Terminal API
- Reader SDK
- Point of Sales API
In-App payment
とIn-Person payment
の違い
◆ In-App payment
- 購入者向けサービス
- アプリ内でクレカを登録し、Readerでの操作無く支払いが可能
- 支払い情報に応じてpayment tokenが発行され、そのtokenをバックエンドに送り、Payment APIを使ってキャプチャされる事で支払いが完了する
- クライアントサイドでIn-App payment SDKでtoken発行 ▶︎ バックエンドでPayment APIを使い、payment tokenをキャプチャ
- squareが用意したUI componentでクレカ登録を行う。UIの多少のカスタマイズは可能
◆ In-Person payment
-
Terminal API
、Reader SDK
、Point of Sales API
で構成される -
Terminal API
: どんなサードパーティPOSアプリからでもSquare ターミナルを使った決済を実現する。クラウドベース。Sandboxが使える。 -
Reader SDK
: square check outフローをアプリに導入する事が出来る。ReaderとStandに対応。Sandboxが使えない。アメリカでのみ使用可能。 -
Point of Sales API
: Squareのアプリをシームレスに起動し、Readerでの決済を可能にする。Sandboxは使えない。
Reader SDKの使用上の制約
色々制約がありそう。気になるのは下記
▶︎ Reader SDKの使用可能エリア★
The Reader SDK is only available for accounts based in the United States. Authorization requests for accounts based outside the United States return an error.
▶︎ Payments APIが使えない?
The Reader SDK does not support the Payments API. Use the Transactions API to get payments generated by the Reader SDK.
▶︎ 決済最低金額は$1
Card payments with the Reader SDK must be $1 or greater. The minimum card payment accepted by the SDK is $1 (100). An attempt to charge a lower amount returns the checkout_amount_below_card_payment_minimum error. The amount limit does not apply to other tender types.
▶︎ 払い戻しは行えない (Refund API使ってってだけ)
The Reader SDK cannot issue refunds. Refunds can be issued programmatically using the Refunds API or manually in the Square Seller Dashboard.
MethodChannel
基礎
▶︎ Dart側
-
MethodChannel('<メソッド名>')
でChannel名を文字列で定義(メソッドとは別) - Channel名は慣習的に
'<bundle name>/<チャネル名>'
で定義 - MethodChannelクラスに定義した
invokeMethod
メソッドで発火したいメソッドの名前を渡す
const final sampleChannel = MethodChannel('com.example.toybox')
sampleChannel.invokeMethod('getBattery')
▶︎ iOS側
-
FlutterMethodChannel
クラスを通して、どのメソッドを実行したいかを受け取る必要がある - そのFlutterMethodChannelクラスを使うには、
FlutterViewController
クラスを定義する必要がある
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
- 更にChannel内にセットされた複数のメソッドのハンドラーをchannelクラスの
setMethodCallHandler
で取得できる - このメソッドがいわばトンネルの様な役割となり、Flutter側で呼び出されたメソッドが流れてくる→handlerではこのcallがFlutterMethodCallクラスとして流れてくる
-
call.method
にはFlutter側で定義したメソッド名が入っているので、そのメソッド名を使って分岐させる -
call.arguments
にはFlutter側で渡した引数が格納されており、Platform側のメソッドに引数を渡す事も出来る - flutter側ではMapとして定義する
final arguments = {'name': 'John'}
- platform側で
as
句でキャストする
guard let args = call.arguments as? [String: String] else {return}
let name = args["name"]
- swiftでは同クラス内に定義されたprivateメソッドの呼び出しは
self
句を使う ex.self.receiveBatteryLevel()
参考
エラーハンドリング
- Dart→Native, Native→Dartの処理が非同期処理として両方に用意されている
- その非同期処理間を
バイナリデータ
のやり取りをしてる - このバイナリデータのエンコード・デコードはDart層、Android層、iOS層それぞれに用意された
MethodCodec
クラスにより行われている - MethodCodecクラスは抽象クラスなので実際にはそれを継承した
StandardMethodCodec
クラス(Android)やFlutterStandardCodec
クラス(iOS)により処理される - このMethodCodecクラスでは処理の成功を
encodeSuccessEnvelope
メソッド、失敗をencodeErrorEnvelope
メソッドとして実装している - Platform側でエラーが起こるとDart側では
PlatformException
がthrowされる - PlatformExceptionクラス
・code → String
・An error code.
・details → dynamic
・Error details, possibly null.
・message → String?
・A human-readable error message, possibly null.
・stacktrace → String?
・Native stacktrace for the error, possibly null.
- iOSのFlutterMethodCodecクラスの
encodeErrorEnvelope
メソッドにはstackTraceを返却する仕組みが存在しない - resultに FlutterMethodNotImplemented がセットされていたら nilを返す
- Dart層で MissingPluginExceptionが発生
- resultに FlutterErrorクラスのインスタンスがセットされていたらエラー
- Dart層で PluginExceptionが発生
- それ以外の場合は成功
- FlutterErrorをresultに明示的に渡した場合は、Dart側で
PluginException
が発生 - エラーハンドリングのすっぽ抜けがあった場合はクラッシュする可能性が高い
参考
Point of Sales APIの実装
Dart側
① MethodChannelのインスタンス化
static const squareChannel = MethodChannel('kiosk.flutter/square');
② MethodChannelのメソッドに渡す引数の生成
- Mapで生成する
final arguments = <String, dynamic>{
'price': 140,
'memo': 'test',
'disablesKeyedInCardEntry': true,
};
③ MethodChannelクラスを使ってメソッドの呼び出し
- 第一引数に呼び出すメソッド名をStringで指定
- 第二引数にMapの引数を指定
- 呼び出すメソッド名はこの後のPlatform側に定義されているメソッドである事
final transactionID =
await squareChannel.invokeMethod<String?>('openSquare', arguments);
全体
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/app_scaffold.dart';
final transactionProvider =
StateProvider.autoDispose<String>((ref) => 'No Transaction Yet');
class SampleScreen extends ConsumerWidget {
const SampleScreen({super.key});
static const squareChannel = MethodChannel('kiosk.flutter/square');
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
color: Colors.grey,
body: Stack(
children: [
Center(
child: Text(
ref.watch(transactionProvider),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Align(
alignment: const Alignment(0, 0.7),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
),
onPressed: () => _openSquareReaderPayment(ref),
child: const Text('OPEN SQUARE'),
),
),
],
),
);
}
Future<void> _openSquareReaderPayment(
WidgetRef ref,
) async {
final arguments = <String, dynamic>{
'price': 140,
'memo': 'test',
'disablesKeyedInCardEntry': true,
};
final transactionID =
await squareChannel.invokeMethod<String?>('openSquare', arguments);
ref
.read(transactionProvider.notifier)
.update((state) => 'Success with $transactionID');
}
}
iOS側
MethodChannel一般
FlutterViewController
をインスタンス化
①- iOS側のMethodChannelを生成に必要な
FlutterViewController
をインスタンス化する。 - Dart側から渡ってくるバイナリデータを変換してくれる?
FlutterMethodChannel
をインスタンス化
② - Dart側でも定義したMethodChannel名と先程インスタンス化した
FlutterViewController
を使って、FlutterMethodChannel
をインスタンス化
MethodCallHandler
を呼び出す
③チャネルの- 指定したチャネルで渡されるメソッドをハンドリングする
setMethodCallHandler
メソッドを呼び出す - コールバックで渡される
FlutterMethodCall
にDart側から呼び出されているメソッド名が渡ってくる - そのメソッド名に応じて条件分岐でどのような処理をplatform側で行うかを記述
- Dart側から渡ってきた引数は
call.arguments
で受け取れる - Dart側に返したい返り値は
FlutterResult
クラスに格納してreturnする
全体
import UIKit
import Flutter
import SquarePointOfSaleSDK
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var final_result: FlutterResult?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let squareChannel = FlutterMethodChannel(name:"kiosk.flutter/square", binaryMessenger: controller.binaryMessenger)
squareChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult)-> Void in
guard call.method == "openSquare" else {
result(FlutterMethodNotImplemented)
return
}
let parameters = call.arguments as! Dictionary<String, Any>
if let price = parameters["price"] as? Int,
let memo=parameters["memo"] as? String,
let disablesKeyedInCardEntry = parameters["disablesKeyedInCardEntry"] as? Bool{
self.openSquare(result: result, price:price, memo:memo, disablesKeyedInCardEntry: disablesKeyedInCardEntry)
}else{
print("パラメータの型が不正です")
result(FlutterMethodNotImplemented)
return
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Square POS API特有
① Square POSアプリに対するリクエストを生成
- callbackURLの設定
「Square Developer Portal > Application > Point of Sale API > iOS app URL schemes」にて設定したURLをcallbackURL
としてセットする
let callbackURL = URL(string: "xxxxx://")!
- 支払い金額の設定
var amount: SCCMoney?
do{
// 支払い金額の設定
amount = try SCCMoney(amountCents: price, currencyCode: "JPY")
}catch {
// エラーハンドリング
}
- アプリケーションIDの設定
「Square Developer Portal > Application > Credentials > Production Application ID」をSCCAPIRequest
クラスにセットする
SCCAPIRequest.setApplicationID("xxxxxxxxxxxxxxx")
- リクエストパラメータの作成
var request: SCCAPIRequest?
do{
request = try SCCAPIRequest(callbackURL: callbackURL,
amount:amount!,
userInfoString: memo, // 注文番号
locationID: nil,
notes:memo,
customerID: nil,
supportedTenderTypes: .all, // 支払い方法の設定
clearsDefaultFees: true, // ???
returnsAutomaticallyAfterPayment: true,
disablesKeyedInCardEntry: disablesKeyedInCardEntry,
skipsReceipt: true
)
}catch{
// エラーハンドリング
}
- Square POSアプリに対するリクエスト
SCCAPIConnection
クラスのperform
メソッドでSquare POSアプリを立ち上げ、リクエストを投げる
var success = false
do{
try SCCAPIConnection.perform(request!)
success = true
}catch let error as NSError{
print(error.localizedDescription)
}
② Square POSアプリからの返答を受け取るメソッドを実装
application(_:open:options:)
メソッドを使って、指定したURLのリソース(この場合Square POSアプリ)を立ち上げ、
③ レスポンスのエラーハンドリング
-
response.error
はそのままではErrorクラスとして返却されるのでNSError
にキャストする事で、エラーコード、エラードメインを取得出来る様になる
if let error = response.error as NSError? {
// Handle a failed request.
print("Error: \(error.localizedDescription)")
}
- Square POSアプリから返却されるエラードメインに応じたエラーコードは文字列ではなくintで返ってくる
- 以下ドキュメントにindex番号順に並んでいる
https://developer.squareup.com/docs/api/point-of-sale#enum-sccapierrorcode
全体
private func openSquare(result:@escaping FlutterResult, price: Int, memo: String, disablesKeyedInCardEntry: Bool){
final_result = result
let callbackURL = URL(string: "square-sample://")!
var amount: SCCMoney?
do{
// 支払い金額の設定
amount = try SCCMoney(amountCents: price, currencyCode: "JPY")
}catch {
// エラーハンドリング
}
SCCAPIRequest.setApplicationID("xxxxxxxxxxxxxxxxxxx")
var request: SCCAPIRequest?
do{
request = try SCCAPIRequest(callbackURL: callbackURL,
amount:amount!,
userInfoString: memo, // 注文番号
locationID: nil,
notes:memo,
customerID: nil,
supportedTenderTypes: .all, // 支払い方法の設定
clearsDefaultFees: true, // ???
returnsAutomaticallyAfterPayment: true,
disablesKeyedInCardEntry: disablesKeyedInCardEntry,
skipsReceipt: true
)
}catch{
// エラーハンドリング
}
// var success = false
do{
try SCCAPIConnection.perform(request!)
// success = true
}catch let error as NSError{
print(error.localizedDescription)
}
}
override func application(_ app:UIApplication, open url: URL, options:[UIApplication.OpenURLOptionsKey: Any]=[:])-> Bool{
// guard SCCAPIResponse.isSquareResponse(url)else{return false}
// var decodeError: Error?
do{
let response = try SCCAPIResponse(responseURL: url)
if let error = response.error{
// Handle a failed request.
print("Error: \(error.localizedDescription)")
}
// Print checkout object
print("Transaction successful: \(response)")
final_result?(response.transactionID)
return true
}catch let error as NSError{
// Handle unexpected errors.
print(error.localizedDescription)
}
return false
}
}
Squareの事前準備
- Square Developer PortalよりApplicationを作成
- 「Application > Point of Sale API > iOS app bundle IDs」にbundle IDを追加
- Square Point of Sale SDKのインストール
- URLスキームの追加
型安全にネイティブとやりとりする