Full implementation example
Last updated
Was this helpful?
Use this code sample to understand how the previous steps fit together.
It initiates contactless payment from:
Double-click (NFCWindowSceneEvent.presentation)
Field detect (NFCWindowSceneEvent.readerDetected)
This sample assumes you already reviewed the individual implementation steps.
Last updated
Was this helpful?
Was this helpful?
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var rootViewController: UIViewController?
let session = ContactlessPaymentSession()
// if double-click or field detect is detected, set to true
var isDefaultPaymentApp = false
// done for iOS 18 where it might need to start emulation on field-detect
var isPaymentOngoing = false
// to support initiate contactless payments by double-click or field-detect action when application is not running:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
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
// set to false each time when it entered
isDefaultPaymentApp = false
if #available(iOS 17.4, *) {
if let nfcEvent = connectionOptions.nfcEvent {
switch nfcEvent {
case .presentation:
// set to true as only been triggered when is default payment application
isDefaultPaymentApp = true
startPayment()
case .readerDetected:
// set to true as only been triggered when is default payment application
isDefaultPaymentApp = true
handleFieldDetect(withCurrentVC: window.rootViewController)
@unknown default: break
}
}
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
// set to false each time when it exits, as you may set another app as default
isDefaultPaymentApp = false
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
if #available(iOS 17.4, *) {
Task {
// default payment app, after session started, will accept APDU and complete payment in background
// this is to cancel it while App goes to the background.
// This action is also recommended when App moves to different screen than the payment screen etc.
await session.cancel()
}
}
}
}
extension SceneDelegate: NFCWindowSceneDelegate {
@available(iOS 17.4, *)
// to support initiate contactless payments by double-click or field-detect action when application is background or foreground:
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 {
// prompt the UI to indicate start the payment by double click
func displayAlertWithDoubleClickStartPayment(withCurrentVC currentVC: UIViewController?) {
let alertController = UIAlertController(title: "Please Double-click to start the payment", message: nil, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "OK", style: .default)
)
currentVC?.present(alertController, animated: false)
}
// return true if require double-click prompt
@available(iOS 17.4, *)
func handleFieldDetect(withCurrentVC currentVC: UIViewController?){
// to tell if it is a normal field detect or we consider as readerDetect for ongoing paymentSession
// only for iOS 18 +
if #available(iOS 18, *), isPaymentOngoing {
// it is iOS 18 + where ongoing payment had received readerDetect event
Task {
try await self.session.startEmulation()
}
} else {
// handle field detect normally on following scenario:
// - iOS 17.4+, regardless payment ongoing OR not ongoing.
// - iOS 18.x, payment not ongoing.
let biometricType = biometricType()
// start payment directly for fingerprint
if biometricType == .touchID {
startPayment()
} else {
displayAlertWithDoubleClickStartPayment(withCurrentVC: currentVC)
}
}
}
func startPayment(withDigitalCardID digitalCardID: String? = nil) {
Task {
// initialize the SDK
try await TSHPay.shared.configure(withVerificationMethod: .userPresence)
if #available(iOS 17.4, *) {
// indicate payment is on-going, it required for field-detect and iOS 18
isPaymentOngoing = false
// checking whether is default payment app and check presentmentIntent whether is valid
if !isDefaultPaymentApp, !PresentmentIntentWrapper.isValid() {
do {
// Not detecting default payment app behavior, we try to obtain presentmentIntentAssertion
try await PresentmentIntentWrapper.acquire()
print("presentmentIntent assertion acquired \(PresentmentIntentWrapper.isValid())")
} catch {
// failed... either due to error or the cool-down
// we have to directly start emulation, at least we wont get app re-directed
}
}
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
// Scenerio A: change card
// if want to startEmulation at Event .authenticationCompleted
// try await session.startEmulation()
break
case .posConnected:
print("StartPayment:: posConnected")
await session.setAlertMessage("POS Connected")
// if application do not acquire NFCPresentmentIntentAssertion, it's not able call startEmulation() in this event.
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")
// display to user
displayTransactionContext(context)
case .errorEncountered(let error):
switch (error as? ContactlessPaymentSession.Error) {
case .cancelled:
// End-user cancel the transaction.
break
case .maxSessionDurationReached:
// Display message to end-user that transaction has timed out.
break
case .nfcPermissionNotAccepted:
// Display message to end-user that permission is not allowed.
break
case .systemEligibilityFailed:
// Display message to end-user that system is not eligible for contactless payment.
// e.g. Apple ID or device location is not in EEA.
break
case .noPaymentKeys:
// Replenishment is required. Refer to Replenishment guide.
break
case .noDefaultCard:
// Default card should be set before transaction.
break
case .keychainError:
// Display error message to end-user
break
case .apduFailure(let reason):
// Retry again
break
case .unknown(let underlying):
// Display error message to end-user
break
case .transmissionError:
// Error encountered in APDU exchange between device and POS reader
break
case .sessionInvalidated:
// The card session has been invalidated by system
break
case .deviceEnvironmentUnsafe(_):
// Device Environment Unsafe error
break
case .setDefaultCardFailure:
// Error encountered when attempting to set the provided Digital Card ID as the default card during the `startPayment(withDigitalCardID:)` API call.
break
case .invalidDigitalCardID:
// Digital Card ID provided is invalid when ``startPayment(withDigitalCardID:)``
break
case .authenticationExpired:
// Error encountered when payment is made after the authentication has been validated.
break
case .posNotSupported:
// Error encountered when POS does not have selected AID in the list.
break
case .cardNotSupported:
// The card is not supported for contactless payment.
break
case .cardNotActive:
// The card selected is not active.
break
case .authenticationFailed:
// Error encountered when the authentication fails.
break
case .authenticationKeyInvalidated(let underlying):
// Authentication key is invalidated because the device passcode is either turned off or reset, triggering a security reset.
// SDK will automatically wipe the stored credentials when this error occurs.
// App must guide user through the re-enrollment process.
// Post a notification to reload the card.
showAlert(message: underlying.localizedDescription) {
NotificationCenter.default.post(name: .didReceiveRequestForReloadCard, object: nil)
}
break
case .biometricNotEnrolled(let message):
// Displays a message to end user to perform biometric enrollment.
showAlert(message: "\(message)")
break
@unknown default:
break
}
@unknown default:
fatalError("fixme: encountered unknown case")
}
}
isPaymentOngoing = false
} else {
// Fallback on earlier versions.
let alertController = UIAlertController(title: "Please use the device with iOS 17.4 or above", 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
}
}