完全な実装例
最終更新
役に立ちましたか?
このコードサンプルを使用して、前の手順がどのように結びつくかを理解してください。
次の方法から非接触決済を開始します:
ダブルクリック (NFCWindowSceneEvent.presentation)
フィールド検出 (NFCWindowSceneEvent.readerDetected)
このサンプルは、個別の実装手順を既に確認していることを前提としています。
最終更新
役に立ちましたか?
役に立ちましたか?
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var rootViewController: UIViewController?
let session = ContactlessPaymentSession()
// ダブルクリックまたはフィールド検出が検出された場合は true に設定
var isDefaultPaymentApp = false
// iOS 18 ではフィールド検出でエミュレーションを開始する必要がある場合があるための処理
var isPaymentOngoing = false
// アプリケーションが実行されていないときに、ダブルクリックまたはフィールド検出アクションで非接触決済を開始できるようにするには:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// このメソッドを使用して、オプションで UIWindow `window` を提供された UIWindowScene `scene` に設定してアタッチします。
// ストーリーボードを使用している場合、`window` プロパティは自動的に初期化されシーンにアタッチされます。
// このデリゲートは、接続されるシーンやセッションが新しいことを意味するものではありません(代わりに `application:configurationForConnectingSceneSession` を参照)。
guard let windowScene = (scene as? UIWindowScene) else { return }
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let landingVC = storyboard.instantiateViewController(identifier: "LandingSB")
let window = UIWindow(windowScene: windowScene)
window.rootViewController = landingVC
window.makeKeyAndVisible()
self.window = window
rootViewController = landingVC
// 入ったときは毎回 false に設定
isDefaultPaymentApp = false
if #available(iOS 17.4, *) {
if let nfcEvent = connectionOptions.nfcEvent {
switch nfcEvent {
case .presentation:
// デフォルトの支払いアプリとしてトリガーされたときのみ true に設定
isDefaultPaymentApp = true
startPayment()
case .readerDetected:
// デフォルトの支払いアプリとしてトリガーされたときのみ true に設定
isDefaultPaymentApp = true
handleFieldDetect(withCurrentVC: window.rootViewController)
@unknown default: break
}
}
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
// 終了したときは毎回 false に設定します。別のアプリをデフォルトに設定する可能性があるためです。
isDefaultPaymentApp = false
// シーンがフォアグラウンドからバックグラウンドに遷移すると呼び出されます。
// このメソッドを使用してデータを保存し、共有リソースを解放し、シーン固有の状態情報を十分に保存します
// シーンを現在の状態に復元できるようにするためです。
if #available(iOS 17.4, *) {
Task {
// デフォルトの支払いアプリは、セッション開始後に APDU を受け入れてバックグラウンドで支払いを完了します
// これはアプリがバックグラウンドに移行する際にそれをキャンセルするための処理です。
// アプリが支払い画面とは別の画面に移動する場合などにもこの操作を推奨します。
await session.cancel()
}
}
}
}
extension SceneDelegate: NFCWindowSceneDelegate {
@available(iOS 17.4, *)
// アプリケーションがバックグラウンドまたはフォアグラウンドにあるときに、ダブルクリックまたはフィールド検出アクションで非接触決済を開始できるようにするには:
func windowScene(_ windowScene: UIWindowScene, didReceiveNFCWindowSceneEvent event: NFCWindowSceneEvent) {
switch event {
case .presentation:
isDefaultPaymentApp = true
startPayment()
case .readerDetected:
isDefaultPaymentApp = true
handleFieldDetect(withCurrentVC: rootViewController)
@unknown default: break
}
}
}
extension SceneDelegate {
// ダブルクリックで支払い開始を示す UI を促す
func displayAlertWithDoubleClickStartPayment(withCurrentVC currentVC: UIViewController?) {
let alertController = UIAlertController(title: "支払いを開始するにはダブルクリックしてください", message: nil, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "OK", style: .default)
)
currentVC?.present(alertController, animated: false)
}
// ダブルクリックのプロンプトが必要なら true を返す
@available(iOS 17.4, *)
func handleFieldDetect(withCurrentVC currentVC: UIViewController?){
// 通常のフィールド検出か、進行中の paymentSession に対する readerDetect と見なすかを判断するため
// iOS 18 のみ
if #available(iOS 18, *), isPaymentOngoing {
// 進行中の支払いが readerDetect イベントを受け取る iOS 18 以降のケースです
Task {
try await self.session.startEmulation()
}
} else {
// 次のシナリオでは通常どおりフィールド検出を処理します:
// - iOS 17.4+、支払いが進行中かどうかに関係なく。
// - iOS 18.x、支払いが進行していない場合。
let biometricType = biometricType()
// 指紋の場合は直接支払いを開始
if biometricType == .touchID {
startPayment()
} else {
displayAlertWithDoubleClickStartPayment(withCurrentVC: currentVC)
}
}
}
func startPayment(withDigitalCardID digitalCardID: String? = nil) {
Task {
// SDK を初期化
try await TSHPay.shared.configure(withVerificationMethod: .userPresence)
if #available(iOS 17.4, *) {
// 支払いが進行中であることを示します。フィールド検出と iOS 18 に必要です。
isPaymentOngoing = false
// デフォルトの支払いアプリかどうか、また presentmentIntent が有効かどうかを確認
if !isDefaultPaymentApp, !PresentmentIntentWrapper.isValid() {
do {
// デフォルトの支払いアプリ動作が検出されない場合、presentmentIntentAssertion を取得しようとします
try await PresentmentIntentWrapper.acquire()
print("presentmentIntent assertion acquired \(PresentmentIntentWrapper.isValid())")
} catch {
// 失敗しました... エラーまたはクールダウンのための可能性があります
// 直接エミュレーションを開始する必要があります。少なくともアプリのリダイレクトは発生しません。
}
}
if let digitalCardID {
print("StartPayment:: with digitalCardID")
await session.startPayment(withDigitalCardID: digitalCardID)
} else {
print("StartPayment:: without digitalCardID")
await session.startPayment()
}
for await event in await session.eventStream {
switch event {
case .authenticationRequired(let action):
print("StartPayment:: authenticationRequired")
Task {
action.proceed()
}
case .authenticationCompleted:
print("StartPayment:: authenticationCompleted")
isPaymentOngoing = true
// シナリオ A: カードを変更
// `.authenticationCompleted` イベントで startEmulation を開始したい場合
// try await session.startEmulation()
break
case .posConnected:
print("StartPayment:: posConnected")
await session.setAlertMessage("POS Connected")
// アプリが NFCPresentmentIntentAssertion を取得していない場合、このイベントで startEmulation() を呼び出すことはできません。
try await session.startEmulation()
break
case .posDisconnected:
print("StartPayment:: posDisconnected")
await session.setAlertMessage("POS Disconnected")
case .transactionCompleted(let context):
print("StartPayment:: transactionCompleted")
await session.setAlertMessage("Transaction Completed")
// ユーザーに表示する
displayTransactionContext(context)
case .errorEncountered(let error):
switch (error as? ContactlessPaymentSession.Error) {
case .cancelled:
// エンドユーザーがトランザクションをキャンセルしました。
break
case .maxSessionDurationReached:
// トランザクションがタイムアウトしたことをエンドユーザーに表示します。
break
case .nfcPermissionNotAccepted:
// 許可が承認されていないことをエンドユーザーに表示します。
break
case .systemEligibilityFailed:
// システムが非接触決済の対象ではないことをエンドユーザーに表示します。
// 例:Apple ID またはデバイスの場所が EEA にない場合。
break
case .noPaymentKeys:
// 補充が必要です。補充ガイドを参照してください。
break
case .noDefaultCard:
// トランザクションの前にデフォルトカードを設定する必要があります。
break
case .keychainError:
// エラーメッセージをエンドユーザーに表示
break
case .apduFailure(let reason):
// 再試行してください
break
case .unknown(let underlying):
// エラーメッセージをエンドユーザーに表示
break
case .transmissionError:
// デバイスと POS リーダー間の APDU 交換でエラーが発生しました
break
case .sessionInvalidated:
// カードセッションがシステムによって無効化されました
break
case .deviceEnvironmentUnsafe(_):
// デバイス環境が安全でないエラー
break
case .setDefaultCardFailure:
// `startPayment(withDigitalCardID:)` API 呼び出し中に、指定されたデジタルカード ID をデフォルトカードとして設定しようとした際に発生したエラー。
break
case .invalidDigitalCardID:
// `startPayment(withDigitalCardID:)` に渡されたデジタルカード ID が無効です。
break
case .authenticationExpired:
// 認証が検証された後に支払いが行われたときに発生したエラー。
break
case .posNotSupported:
// POS に選択された AID がリストにないときに発生するエラー。
break
case .cardNotSupported:
// 非接触決済でそのカードはサポートされていません。
break
case .cardNotActive:
// 選択されたカードはアクティブではありません。
break
case .authenticationFailed:
// 認証が失敗したときに発生するエラー。
break
case .authenticationKeyInvalidated(let underlying):
// デバイスのパスコードがオフになっているかリセットされたために認証キーが無効化され、セキュリティリセットが発生したことが原因です。
// このエラーが発生した場合、SDK は保存された認証情報を自動的に消去します。
// アプリはユーザーに再登録の手順を案内する必要があります。
// カードを再読み込みするための通知を送信してください。
showAlert(message: underlying.localizedDescription) {
NotificationCenter.default.post(name: .didReceiveRequestForReloadCard, object: nil)
}
break
case .biometricNotEnrolled(let message):
// ユーザーに生体認証の登録を行うよう促すメッセージを表示します。
showAlert(message: "\(message)")
break
@unknown default:
break
}
@unknown default:
fatalError("fixme: encountered unknown case")
}
}
isPaymentOngoing = false
} else {
// 以前のバージョンのフォールバック。
let alertController = UIAlertController(title: "iOS 17.4 以上のデバイスを使用してください", message: nil, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "OK", style: .default)
)
rootViewController?.present(alertController, animated: false)
}
}
}
func biometricType() -> LABiometryType {
let authContext = LAContext()
if #available(iOS 11, *) {
authContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
return authContext.biometryType
}
return LABiometryType.none
}
}