Ejemplo completo de implementación
Última actualización
¿Te fue útil?
Use esta muestra de código para entender cómo encajan los pasos anteriores.
Inicia el pago sin contacto desde:
Doble clic (NFCWindowSceneEvent.presentation)
Detección de campo (NFCWindowSceneEvent.readerDetected)
Este ejemplo asume que ya revisaste 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 podría necesitar iniciar la emulación en la detección de campo
var isPaymentOngoing = false
// para admitir el inicio de pagos sin contacto mediante doble clic o acción de detección de campo cuando la aplicación no está en ejecución:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Usa este método para configurar opcionalmente y adjuntar la UIWindow `window` a la UIWindowScene `scene` proporcionada.
// Si usas 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 que se conectan sean nuevas (consulta en su lugar `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, ya que solo se activa cuando es la aplicación de pago predeterminada
isDefaultPaymentApp = true
startPayment()
case .readerDetected:
// establecer en true, ya que solo se activa cuando es la 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 podrías establecer otra aplicación como predeterminada
isDefaultPaymentApp = false
// Se llama cuando la escena pasa del primer plano al fondo.
// Usa este método para guardar datos, liberar recursos compartidos y almacenar suficiente 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 iniciar la sesión, aceptará APDU y completará el pago en segundo plano
// esto es para cancelarlo mientras la app va al fondo.
// Esta acción también se recomienda cuando la app pasa a una pantalla diferente de la pantalla de pago, etc.
await session.cancel()
}
}
}
}
extension SceneDelegate: NFCWindowSceneDelegate {
@available(iOS 17.4, *)
// para admitir el inicio de pagos sin contacto mediante doble clic o acción de detección de campo cuando la aplicación está en segundo plano o en 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 {
// mostrar en la UI que el pago se inicia con doble clic
func displayAlertWithDoubleClickStartPayment(withCurrentVC currentVC: UIViewController?) {
let alertController = UIAlertController(title: "Por favor, haz doble clic para iniciar el pago", message: nil, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "OK", style: .default)
)
currentVC?.present(alertController, animated: false)
}
// devuelve true si 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 la consideramos readerDetect para una paymentSession en curso
// solo para iOS 18 +
if #available(iOS 18, *), isPaymentOngoing {
// es iOS 18 + donde el pago en curso ya recibió el evento readerDetect
Task {
try await self.session.startEmulation()
}
} else {
// manejar la detección de campo normalmente en el siguiente escenario:
// - 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 digital
if biometricType == .touchID {
startPayment()
} else {
displayAlertWithDoubleClickStartPayment(withCurrentVC: currentVC)
}
}
}
func startPayment(withDigitalCardID digitalCardID: String? = nil) {
Task {
// inicializa el SDK
try await TSHPay.shared.configure(withVerificationMethod: .userPresence)
if #available(iOS 17.4, *) {
// indica que el pago está en curso, es necesario para la detección de campo y iOS 18
isPaymentOngoing = false
// comprobar si es la aplicación de pago predeterminada y verificar si presentmentIntent es válido
if !isDefaultPaymentApp, !PresentmentIntentWrapper.isValid() {
do {
// No detecta el comportamiento de la 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 período de enfriamiento
// tenemos que iniciar la emulación directamente; al menos no nos redirigirán la app
}
}
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 tarjeta
// si quieres iniciarEmulation en Event .authenticationCompleted
// try await session.startEmulation()
break
case .posConnected:
print("StartPayment:: posConnected")
await session.setAlertMessage("POS conectado")
// si la aplicación no adquiere NFCPresentmentIntentAssertion, no podrá llamar a startEmulation() en este evento.
try await session.startEmulation()
break
case .posDisconnected:
print("StartPayment:: posDisconnected")
await session.setAlertMessage("POS desconectado")
case .transactionCompleted(let context):
print("StartPayment:: transactionCompleted")
await session.setAlertMessage("Transacción completada")
// mostrar al usuario
displayTransactionContext(context)
case .errorEncountered(let error):
switch (error as? ContactlessPaymentSession.Error) {
case .cancelled:
// El usuario final cancela la transacción.
break
case .maxSessionDurationReached:
// Muestra un mensaje al usuario final de que la transacción ha caducado.
break
case .nfcPermissionNotAccepted:
// Muestra un mensaje al usuario final de que el permiso no está permitido.
break
case .systemEligibilityFailed:
// Muestra un mensaje al usuario final de que el sistema no es elegible para el pago sin contacto.
// por ejemplo, Apple ID o la ubicación del dispositivo no está en el EEE.
break
case .noPaymentKeys:
// Se requiere reposición. Consulta la guía de reposición.
break
case .noDefaultCard:
// La tarjeta predeterminada debe establecerse antes de la transacción.
break
case .keychainError:
// Muestra un mensaje de error al usuario final
break
case .apduFailure(let reason):
// Intenta de nuevo
break
case .unknown(let underlying):
// Muestra un 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 inseguro del dispositivo
break
case .setDefaultCardFailure:
// Error encontrado al intentar establecer el Digital Card ID proporcionado como tarjeta predeterminada durante la llamada a la API `startPayment(withDigitalCardID:)`.
break
case .invalidDigitalCardID:
// El Digital Card ID proporcionado no es válido cuando se usa ``startPayment(withDigitalCardID:)``
break
case .authenticationExpired:
// Error encontrado cuando el pago se realiza después de que la autenticación haya 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 el pago sin contacto.
break
case .cardNotActive:
// La tarjeta seleccionada no está activa.
break
case .authenticationFailed:
// Error encontrado cuando falla la autenticación.
break
case .authenticationKeyInvalidated(let underlying):
// La clave de autenticación se invalida porque el código del dispositivo está desactivado o se restablece, lo que desencadena un restablecimiento de seguridad.
// El SDK borrará automáticamente las credenciales almacenadas cuando ocurra este error.
// La app debe guiar al usuario a través del proceso de nuevo registro.
// Publica 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 el registro biométrico.
showAlert(message: "\(message)")
break
@unknown default:
break
}
@unknown default:
fatalError("fixme: encountered unknown case")
}
}
isPaymentOngoing = false
} else {
// Alternativa en versiones anteriores.
let alertController = UIAlertController(title: "Por favor, usa un 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
}
}