完全な実装例
最終更新
役に立ちましたか?
最終更新
役に立ちましたか?
役に立ちましたか?
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: カードを変更
// Event .authenticationCompleted で startEmulation したい場合
// try await session.startEmulation()
break
case .posConnected:
print("StartPayment:: posConnected")
await session.setAlertMessage("POS 接続済み")
// アプリケーションが NFCPresentmentIntentAssertion を取得していない場合、このイベントで startEmulation() を呼び出すことはできません。
try await session.startEmulation()
break
case .posDisconnected:
print("StartPayment:: posDisconnected")
await session.setAlertMessage("POS 切断済み")
case .transactionCompleted(let context):
print("StartPayment:: transactionCompleted")
await session.setAlertMessage("取引完了")
// ユーザーに表示
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 呼び出し中に、提供された Digital Card ID をデフォルトカードとして設定しようとしてエラーが発生しました。
break
case .invalidDigitalCardID:
// ``startPayment(withDigitalCardID:)`` で提供された Digital Card 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: 未知のケースに遭遇しました")
}
}
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
}
}