Prima di implementare una gestione reale per pagamenti, storni o qualsiasi altra operazione, vale la pena capire il modello unico che governa tutta la comunicazione con l'applicazione di pagamento Market Pay. Ogni operazione nell'SDK lo segue. Una volta compreso, tutta l'API diventa prevedibile.
Questa guida spiega tre cose: perché le risposte non tornano dal metodo che chiami, come distinguere le risposte quando arrivano tutte nello stesso posto, e perché gestisci i super-tipi delle risposte — non i loro sottotipi di successo.
Le richieste sono fire-and-forget; le risposte arrivano separatamente
Quando chiami qualcosa come sendPaymentRequest, potresti aspettarti che restituisca il risultato del pagamento. Non lo fa. La chiamata invia la richiesta all'applicazione di pagamento e ritorna quasi immediatamente — non attende che il terminale elabori la carta.
Il risultato effettivo arriva più tardi, in modo asincrono, nell'interceptor che hai registrato quando hai inizializzato l'SDK. Questa è la cosa più importante da interiorizzare: il metodo che chiami e la risposta che gestisci sono in due posti diversi nel tuo codice.
// Invia qui…
viewModelScope.launch {
clientSDK.sendPaymentRequest(/* … */)
}
// …e il risultato arriva qui, più tardi, nell'interceptor:
.bindInterceptor(
interceptor = { message ->
// la risposta di pagamento appare come `message`
Result.success(message)
}
)
Perché è costruito così? Un pagamento con carta non è istantaneo — il terminale chiede al titolare della carta, comunica con l'acquirer e può richiedere diversi secondi. Bloccare la chiamata per tutta la durata congelerebbe la tua app. Scollegare la richiesta dalla risposta permette al terminale di impiegare tutto il tempo necessario mentre la tua app resta reattiva, e consente all'app di pagamento di inviarti aggiornamenti intermedi (vedi "richieste di visualizzazione" sotto) prima del risultato finale.
Ogni risposta è un DomainMessage
L'interceptor riceve un singolo parametro tipizzato come DomainMessage. Ogni messaggio nella comunicazione App2App — risposte di login, risposte di pagamento, errori, aggiornamenti di progresso — è un sottotipo di DomainMessage. Ecco perché un solo interceptor può riceverli tutti.
Poiché arrivano tutti come lo stesso tipo generale, il tuo compito nell'interceptor è controllare il tipo del messaggio in arrivo per decidere cosa è e come gestirlo:
.bindInterceptor(
interceptor = { message ->
when (message) {
is SuccessRetailerLoginResponse -> { /* la sessione è ora aperta */ }
is SuccessRetailerPaymentResponse -> { /* pagamento approvato */ }
is ErrorRetailerPaymentResponse -> { /* pagamento fallito */ }
// …gestisci i tipi di messaggio che interessano alla tua app
}
Result.success(message)
}
)
Devi solo abbinare i tipi di messaggio su cui la tua applicazione effettivamente agisce. I messaggi che non corrispondono semplicemente vengono ignorati — ma restituisci sempre Result.success(message) alla fine affinché l'SDK sappia che il messaggio è stato accettato.
Ogni operazione ha il proprio super-tipo di risposta
Le risposte non sono un elenco piatto di classi non correlate. Ogni operazione ha un super-tipo sigillato, sotto cui si trovano i risultati concreti — tipicamente una variante di successo e una di errore. Abbinare il super-tipo cattura ogni esito di quell'operazione; abbinare un singolo sottotipo cattura solo uno.
| Operazione | Super-tipo di risposta | Sottotipi che potresti ricevere |
|---|---|---|
| Login | RetailerLoginResponse |
SuccessRetailerLoginResponse, ErrorRetailerLoginResponse
|
| Pagamento | RetailerPaymentResponse |
SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse, PartialRetailerPaymentResponse
|
| Storno | RetailerReversalResponse |
SuccessRetailerReversalResponse, ErrorRetailerReversalResponse
|
| Riconciliazione | RetailerReconciliationResponse |
Success…, Error…
|
| Stato transazione | RetailerTransactionStatusResponse |
Success…, Error…
|
| Acquisizione carta | RetailerAcquisitionResponse |
SuccessRetailerAcquisitionResponse, ErrorRetailerAcquisitionResponse
|
| Diagnosi (test connessione) | RetailerDiagnosisResponse |
Success…, Error…
|
| Logout | RetailerLogoutResponse |
Success…, Error…
|
Nota. Il pagamento ha un terzo sottotipo,
PartialRetailerPaymentResponse, che le altre operazioni non hanno. Ecco esattamente perché ragioni in termini di super-tipo: un gestore scritto solo per "successo o errore" perderebbe il caso parziale. L'elenco completo e autorevole dei sottotipi per ogni operazione si trova nel riferimento API (sdk-doc.zip) — in caso di dubbio, abbina il super-tipo.
Il pagamento invia anche aggiornamenti di progresso
Durante un pagamento, prima del risultato finale, l'applicazione di pagamento può inviarti messaggi RetailerDisplayRequest. Questi non sono l'esito del pagamento — ti dicono cosa sta facendo il terminale in quel momento (attesa della carta, contatto con l'host, ecc.), utile se vuoi mostrare una tua interfaccia di progresso. La stessa richiesta che produce una RetailerPaymentResponse può produrre diversi messaggi RetailerDisplayRequest lungo il percorso. Gestiscili se vuoi un progresso in tempo reale; ignorali altrimenti. L'esito finale è sempre uno dei sottotipi di RetailerPaymentResponse.
Perché attendi il super-tipo, non il sottotipo di successo
Questo è l'errore che causa i bug più confusi, quindi ha una sezione a sé. È importante soprattutto quando passi dalla modalità fire-and-forget a quella bloccante — attendendo una risposta specifica prima di continuare.
L'SDK fornisce BlockingMessageGateway per questo. Invi ogni messaggio dell'interceptor a esso, quindi puoi inviare una richiesta e sospendere la tua coroutine finché non arriva la risposta corrispondente:
val gateway = BlockingMessageGateway()
// Nell'interceptor, passa ogni messaggio al gateway:
.bindInterceptor(
interceptor = { message ->
gateway.onNewMessage(message)
Result.success(message)
}
)
// Poi invia e attendi, specificando il tipo di risposta da attendere:
val response: RetailerLoginResponse = gateway.sendBlocking(
timeoutMillis = 30000
) {
clientSDK.sendLoginRequest()
}
when (response) {
is SuccessRetailerLoginResponse -> { /* effettuato il login */ }
is ErrorRetailerLoginResponse -> { /* login rifiutato — gestiscilo */ }
}
Il tipo che metti tra parentesi angolari / assegni è il tipo che sendBlocking attende. Ecco la trappola:
-
Attendi
RetailerLoginResponse(il super-tipo) → la coroutine riprende su successo o errore. Gestisci entrambi. Corretto. -
Attendi
SuccessRetailerLoginResponse(il sottotipo di successo) → la coroutine riprende solo se il login ha successo. Se il login fallisce, non arriva mai unSuccessRetailerLoginResponse, il gateway continua ad aspettare e la tua coroutine rimane bloccata finché non genera unTimeoutCancellationException. La risposta di errore è stata consegnata — semplicemente non la stavi ascoltando.
In altre parole: attendere il sottotipo di successo trasforma silenziosamente ogni fallimento in un timeout. Attendi sempre il super-tipo così entrambi gli esiti fanno riprendere la coroutine, poi usa un when come mostrato sopra.
sendBlocking ha un timeout predefinito di 60 secondi (timeoutMillis = 60000). Passa un valore più breve per operazioni che devono fallire rapidamente. Qualunque sia il timeout, racchiudi la chiamata per poter gestire TimeoutCancellationException — un timeout significa che nessuna risposta è arrivata in tempo, condizione a cui la tua app deve reagire.
Il modello in un paragrafo
Invii una richiesta e questa ritorna immediatamente. La risposta — e ogni aggiornamento di progresso prima di essa — arriva più tardi nel tuo interceptor, tutti tipizzati come DomainMessage. Controlli il tipo per trovare quelli che ti interessano. Ogni operazione ha un super-tipo di risposta sigillato che copre tutti i suoi esiti; ragioni e (in modalità bloccante) attendi in termini di quel super-tipo, mai di un singolo sottotipo, così i fallimenti raggiungono il tuo codice invece di perdersi in un timeout. Ogni operazione nell'SDK funziona esattamente così — quindi una volta che ne hai gestita una, le hai gestite tutte.
Articoli correlati
- Quickstart — il primo pagamento end-to-end su cui si basa questo modello.
- Ciclo di vita sessione e login — perché il login deve riuscire prima che qualsiasi risposta sia significativa, e quando effettuare nuovamente il login.
- Gestione stato e foreground — l'altro canale asincrono (l'osservatore di eventi), separato dall'interceptor descritto qui.
- Risoluzione problemi: sendBlocking rimane bloccato fino al timeout — la modalità di errore che l'ultima sezione di questa guida serve a prevenire.
-
Riferimento API (
sdk-doc.zip) — l'elenco autorevole e completo dei sottotipi per ogni operazione.