Ejemplo de implementación completa
Última actualización
¿Te fue útil?
Use este ejemplo de código para entender cómo encajan los pasos anteriores.
Inicia el pago sin contacto desde:
Haga doble clic (NFCWindowSceneEvent.presentation)
Detección de campo (NFCWindowSceneEvent.readerDetected)
Este ejemplo asume que ya revisó los pasos de implementación individuales.
Última actualización
¿Te fue útil?
¿Te fue útil?
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var rootViewController: UIViewController?
let session = ContactlessPaymentSession()
// si se detecta doble clic o detección de campo, establecer en true
var isDefaultPaymentApp = false
// hecho para iOS 18 donde puede necesitar iniciar la emulación en detección de campo
var isPaymentOngoing = false
// para admitir iniciar pagos sin contacto mediante acción de doble clic o detección de campo cuando la aplicación no se está ejecutando:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use este método para configurar opcionalmente y adjuntar la UIWindow `window` a la UIWindowScene `scene` proporcionada.
// Si usa un storyboard, la propiedad `window` se inicializará y adjuntará automáticamente a la escena.
// Este delegado no implica que la escena o la sesión de conexión sean nuevas (vea `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
// establecer en false cada vez que entra
isDefaultPaymentApp = false
if #available(iOS 17.4, *) {
if let nfcEvent = connectionOptions.nfcEvent {
switch nfcEvent {
case .presentation:
// establecer en true solo cuando se haya activado como aplicación de pago predeterminada
isDefaultPaymentApp = true
startPayment()
case .readerDetected:
// establecer en true solo cuando se haya activado como aplicación de pago predeterminada
isDefaultPaymentApp = true
handleFieldDetect(withCurrentVC: window.rootViewController)
@unknown default: break
}
}
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
// establecer en false cada vez que sale, ya que puede configurar otra aplicación como predeterminada
isDefaultPaymentApp = false
// Llamado cuando la escena pasa del primer plano al fondo.
// Use este método para guardar datos, liberar recursos compartidos y almacenar información de estado específica de la escena
// para restaurar la escena a su estado actual.
if #available(iOS 17.4, *) {
Task {
// la aplicación de pago predeterminada, después de que la sesión haya comenzado, aceptará APDU y completará el pago en segundo plano
// esto es para cancelarlo cuando la aplicación pasa a segundo plano.
// También se recomienda esta acción cuando la aplicación cambia a una pantalla diferente a la de pago, etc.
await session.cancel()
}
}
}
}
extension SceneDelegate: NFCWindowSceneDelegate {
@available(iOS 17.4, *)
// para admitir iniciar pagos sin contacto mediante acción de doble clic o detección de campo cuando la aplicación está en segundo plano o primer plano:
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 {
// solicitar a la interfaz de usuario que indique iniciar el pago mediante doble clic
func displayAlertWithDoubleClickStartPayment(withCurrentVC currentVC: UIViewController?) {
let alertController = UIAlertController(title: "Por favor, haga doble clic para iniciar el pago", message: nil, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "OK", style: .default)
)
currentVC?.present(alertController, animated: false)
}
// devolver true si se requiere el aviso de doble clic
@available(iOS 17.4, *)
func handleFieldDetect(withCurrentVC currentVC: UIViewController?){
// para indicar si es una detección de campo normal o lo consideramos readerDetect para una sesión de pago en curso
// solo para iOS 18 +
if #available(iOS 18, *), isPaymentOngoing {
// es iOS 18 + donde el pago en curso ha recibido el evento readerDetect
Task {
try await self.session.startEmulation()
}
} else {
// manejar la detección de campo normalmente en los siguientes escenarios:
// - iOS 17.4+, independientemente de si el pago está en curso o no.
// - iOS 18.x, pago no en curso.
let biometricType = biometricType()
// iniciar el pago directamente para huella dactilar
if biometricType == .touchID {
startPayment()
} else {
displayAlertWithDoubleClickStartPayment(withCurrentVC: currentVC)
}
}
}
func startPayment(withDigitalCardID digitalCardID: String? = nil) {
Task {
// inicializar el SDK
try await TSHPay.shared.configure(withVerificationMethod: .userPresence)
if #available(iOS 17.4, *) {
// indicar que el pago está en curso, es requerido para detección de campo e iOS 18
isPaymentOngoing = false
// comprobando si es la aplicación de pago predeterminada y verificar si presentmentIntent es válido
if !isDefaultPaymentApp, !PresentmentIntentWrapper.isValid() {
do {
// No se detecta el comportamiento de aplicación de pago predeterminada, intentamos obtener presentmentIntentAssertion
try await PresentmentIntentWrapper.acquire()
print("presentmentIntent assertion acquired \(PresentmentIntentWrapper.isValid())")
} catch {
// falló... ya sea por error o por el tiempo de enfriamiento
// tenemos que iniciar la emulación directamente, al menos no nos redirigirán la aplicación
}
}
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
// Escenario A: cambiar de tarjeta
// si desea iniciar la emulación en el evento .authenticationCompleted
// try await session.startEmulation()
break
case .posConnected:
print("StartPayment:: posConnected")
await session.setAlertMessage("POS Connected")
// si la aplicación no adquiere NFCPresentmentIntentAssertion, no puede llamar a startEmulation() en este evento.
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")
// mostrar al usuario
displayTransactionContext(context)
case .errorEncountered(let error):
switch (error as? ContactlessPaymentSession.Error) {
case .cancelled:
// El usuario final canceló la transacción.
break
case .maxSessionDurationReached:
// Mostrar un mensaje al usuario final de que la transacción ha expirado.
break
case .nfcPermissionNotAccepted:
// Mostrar un mensaje al usuario final de que el permiso no está permitido.
break
case .systemEligibilityFailed:
// Mostrar un mensaje al usuario final de que el sistema no es elegible para pagos sin contacto.
// p. ej., Apple ID o la ubicación del dispositivo no está en el EEE.
break
case .noPaymentKeys:
// Se requiere reabastecimiento. Consulte la guía de reabastecimiento.
break
case .noDefaultCard:
// La tarjeta predeterminada debe establecerse antes de la transacción.
break
case .keychainError:
// Mostrar mensaje de error al usuario final
break
case .apduFailure(let reason):
// Reintentar
break
case .unknown(let underlying):
// Mostrar mensaje de error al usuario final
break
case .transmissionError:
// Error encontrado en el intercambio APDU entre el dispositivo y el lector POS
break
case .sessionInvalidated:
// La sesión de la tarjeta ha sido invalidada por el sistema
break
case .deviceEnvironmentUnsafe(_):
// Error de entorno de dispositivo inseguro
break
case .setDefaultCardFailure:
// Error encontrado al intentar establecer la ID de Tarjeta Digital proporcionada como tarjeta predeterminada durante la llamada a la API `startPayment(withDigitalCardID:)`.
break
case .invalidDigitalCardID:
// La ID de Tarjeta Digital proporcionada no es válida en ``startPayment(withDigitalCardID:)``
break
case .authenticationExpired:
// Error encontrado cuando el pago se realiza después de que la autenticación ha sido validada.
break
case .posNotSupported:
// Error encontrado cuando el POS no tiene el AID seleccionado en la lista.
break
case .cardNotSupported:
// La tarjeta no es compatible con pagos sin contacto.
break
case .cardNotActive:
// La tarjeta seleccionada no está activa.
break
case .authenticationFailed:
// Error encontrado cuando la autenticación falla.
break
case .authenticationKeyInvalidated(let underlying):
// La clave de autenticación se invalida porque el código de acceso del dispositivo se apagó o se restableció, lo que desencadena un restablecimiento de seguridad.
// El SDK borrará automáticamente las credenciales almacenadas cuando ocurra este error.
// La aplicación debe guiar al usuario a través del proceso de reinscripción.
// Publique una notificación para recargar la tarjeta.
showAlert(message: underlying.localizedDescription) {
NotificationCenter.default.post(name: .didReceiveRequestForReloadCard, object: nil)
}
break
case .biometricNotEnrolled(let message):
// Muestra un mensaje al usuario final para realizar la inscripción biométrica.
showAlert(message: "\(message)")
break
@unknown default:
break
}
@unknown default:
fatalError("fixme: encountered unknown case")
}
}
isPaymentOngoing = false
} else {
// Compatibilidad con versiones anteriores.
let alertController = UIAlertController(title: "Por favor use el dispositivo con iOS 17.4 o superior", 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
}
}