> For the complete documentation index, see [llms.txt](https://docs.payments.thalescloud.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.payments.thalescloud.io/nfc-wallet-sdk-ios/implement-nfc-wallet/make-payments/implement-contactless-payment/full-implementation-example.md).

# Full implementation example

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`)

{% hint style="info" %}
This sample assumes you already reviewed the individual implementation steps.
{% endhint %}

### Full source code

{% code title="SceneDelegate.swift" expandable="true" %}

```swift
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
    }
}
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.payments.thalescloud.io/nfc-wallet-sdk-ios/implement-nfc-wallet/make-payments/implement-contactless-payment/full-implementation-example.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
