Voordat je echte afhandeling bouwt voor betalingen, terugboekingen of een andere operatie, is het de moeite waard om het ene patroon te begrijpen dat alle communicatie met de Market Pay betaalapplicatie regelt. Elke operatie in de SDK volgt dit patroon. Zodra dit duidelijk is, wordt de hele API voorspelbaar.
Deze gids legt drie dingen uit: waarom antwoorden niet terugkomen van de methode die je aanroept, hoe je antwoorden uit elkaar houdt wanneer ze allemaal op dezelfde plaats aankomen, en waarom je de antwoord-supertypes behandelt — en niet hun succesvolle subtypes.
Verzoeken zijn fire-and-forget; antwoorden komen apart binnen
Wanneer je iets aanroept zoals sendPaymentRequest, zou je kunnen verwachten dat het het betalingsresultaat teruggeeft. Dat doet het niet. De oproep stuurt het verzoek naar de betaalapplicatie en keert bijna onmiddellijk terug — het wacht niet tot de terminal de kaart verwerkt.
Het daadwerkelijke resultaat komt later, asynchroon, binnen in de interceptor die je registreerde toen je de SDK initialiseerde. Dit is het allerbelangrijkste om te onthouden: de methode die je aanroept en het antwoord dat je afhandelt bevinden zich op twee verschillende plekken in je code.
// Je verstuurt hier…
viewModelScope.launch {
clientSDK.sendPaymentRequest(/* … */)
}
// …en het resultaat komt hier later binnen, in de interceptor:
.bindInterceptor(
interceptor = { message ->
// het betalingsantwoord verschijnt als `message`
Result.success(message)
}
)
Waarom is het zo opgebouwd? Een kaartbetaling is niet direct — de terminal vraagt de kaarthouder, communiceert met de acquirer en kan meerdere seconden duren. Je oproep blokkeren voor de gehele duur zou je app laten vastlopen. Het loskoppelen van het verzoek van het antwoord laat de terminal zoveel tijd nemen als nodig is terwijl je app responsief blijft, en het laat de betaalapplicatie je tussenliggende updates sturen (zie "display requests" hieronder) vóór het uiteindelijke resultaat.
Elk antwoord is een DomainMessage
De interceptor ontvangt één parameter getypt als DomainMessage. Elk bericht in App2App-communicatie — login-antwoorden, betalingsantwoorden, fouten, voortgangsupdates — is een subtype van DomainMessage. Daarom kan één interceptor ze allemaal ontvangen.
Omdat ze allemaal binnenkomen als hetzelfde brede type, is jouw taak in de interceptor om het binnenkomende bericht te type-checken om te bepalen wat het is en hoe je het afhandelt:
.bindInterceptor(
interceptor = { message ->
when (message) {
is SuccessRetailerLoginResponse -> { /* sessie is nu open */ }
is SuccessRetailerPaymentResponse -> { /* betaling goedgekeurd */ }
is ErrorRetailerPaymentResponse -> { /* betaling mislukt */ }
// …handel de berichttypes af die je app belangrijk vindt
}
Result.success(message)
}
)
Je hoeft alleen de berichttypes te matchen waarop je applicatie daadwerkelijk reageert. Berichten die je niet matcht, vallen gewoon door — maar geef altijd Result.success(message) terug aan het einde zodat de SDK weet dat het bericht is geaccepteerd.
Elke operatie heeft zijn eigen antwoord-supertype
De antwoorden zijn geen vlakke lijst van niet-verwante klassen. Elke operatie heeft een sealed supertype, en daaronder zitten de concrete uitkomsten — meestal een succes- en een foutvariant. Het matchen van het supertype vangt elke uitkomst van die operatie; het matchen van één subtype vangt slechts één uitkomst.
| Operatie | Antwoord supertype | Subtypes die je kunt ontvangen |
|---|---|---|
| Login | RetailerLoginResponse |
SuccessRetailerLoginResponse, ErrorRetailerLoginResponse
|
| Betaling | RetailerPaymentResponse |
SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse, PartialRetailerPaymentResponse
|
| Terugboeking | RetailerReversalResponse |
SuccessRetailerReversalResponse, ErrorRetailerReversalResponse
|
| Afstemming | RetailerReconciliationResponse |
Success…, Error…
|
| Transactie status | RetailerTransactionStatusResponse |
Success…, Error…
|
| Kaartacquisitie | RetailerAcquisitionResponse |
SuccessRetailerAcquisitionResponse, ErrorRetailerAcquisitionResponse
|
| Diagnose (test verbinding) | RetailerDiagnosisResponse |
Success…, Error…
|
| Uitloggen | RetailerLogoutResponse |
Success…, Error…
|
Opmerking. Betaling heeft een derde subtype,
PartialRetailerPaymentResponse, die de andere operaties niet hebben. Dit is precies waarom je redeneert in termen van het supertype: een handler geschreven alleen voor "succes of fout" zou het gedeeltelijke geval missen. De volledige, gezaghebbende lijst van subtypes voor elke operatie staat in de API-referentie (sdk-doc.zip) — bij twijfel, match het supertype.
Betaling stuurt ook voortgangsupdates
Tijdens een betaling, vóór het definitieve resultaat, kan de betaalapplicatie je RetailerDisplayRequest berichten sturen. Dit zijn niet het betalingsresultaat — ze vertellen je wat de terminal op dat moment doet (wachten op de kaart, contact maken met de host, enzovoort), wat handig is als je je eigen voortgangs-UI wilt tonen. Hetzelfde verzoek dat een RetailerPaymentResponse produceert, kan meerdere RetailerDisplayRequest berichten onderweg produceren. Handel ze af als je live voortgang wilt; negeer ze anders. De uiteindelijke uitkomst is altijd één van de RetailerPaymentResponse subtypes.
Waarom je wacht op het supertype, niet op het succes-subtype
Dit is de fout die de meeste verwarrende bugs veroorzaakt, dus het krijgt een eigen sectie. Het is vooral belangrijk wanneer je overschakelt van fire-and-forget naar blokkerende modus — wachten op een specifiek antwoord voordat je doorgaat.
De SDK biedt BlockingMessageGateway hiervoor. Je voert elk interceptorbericht daarin in, en dan kun je een verzoek sturen en je coroutine pauzeren totdat het bijpassende antwoord binnenkomt:
val gateway = BlockingMessageGateway()
// In de interceptor, geef elk bericht door aan de gateway:
.bindInterceptor(
interceptor = { message ->
gateway.onNewMessage(message)
Result.success(message)
}
)
// Dan stuur-en-wacht, waarbij je het antwoordtype opgeeft om op te wachten:
val response: RetailerLoginResponse = gateway.sendBlocking(
timeoutMillis = 30000
) {
clientSDK.sendLoginRequest()
}
when (response) {
is SuccessRetailerLoginResponse -> { /* ingelogd */ }
is ErrorRetailerLoginResponse -> { /* login geweigerd — afhandelen */ }
}
Het type dat je tussen de hoekhaken zet / waaraan je toewijst is het type waarop sendBlocking wacht. Hier zit de valkuil:
-
Wacht op
RetailerLoginResponse(het supertype) → de coroutine gaat verder bij zowel succes als fout. Je handelt beide af. Correct. -
Wacht op
SuccessRetailerLoginResponse(het succes-subtype) → de coroutine gaat alleen verder als de login slaagt. Als de login faalt, komt er nooit eenSuccessRetailerLoginResponse, blijft de gateway wachten, en hangt je coroutine totdat het uiteindelijk eenTimeoutCancellationExceptiongooit. Het foutantwoord werd wel geleverd — je luisterde er alleen niet naar.
Met andere woorden: wachten op het succes-subtype zet elke fout stilletjes om in een timeout. Wacht altijd op het supertype zodat beide uitkomsten je coroutine hervatten, en vertak dan met een when zoals hierboven getoond.
sendBlocking heeft standaard een timeout van 60 seconden (timeoutMillis = 60000). Geef een kortere waarde door voor operaties die snel moeten falen. Wat de timeout ook is, wikkel de oproep zodat je TimeoutCancellationException kunt afhandelen — een timeout betekent dat er geen antwoord op tijd kwam, wat op zich een situatie is waarop je app moet reageren.
Het patroon in één alinea
Je verstuurt een verzoek en het keert onmiddellijk terug. Het antwoord — en eventuele voortgangsupdates daarvoor — komen later binnen in je interceptor, allemaal getypt als DomainMessage. Je type-checkt om de berichten te vinden die je belangrijk vindt. Elke operatie heeft een sealed antwoord-supertype dat al zijn uitkomsten dekt; je redeneert en (in blokkerende modus) wacht in termen van dat supertype, nooit een enkel subtype, zodat fouten je code bereiken in plaats van te verdwijnen in een timeout. Elke operatie in de SDK werkt precies zo — dus zodra je er één hebt afgehandeld, heb je ze allemaal afgehandeld.
Gerelateerd
- Quickstart — de end-to-end eerste betaling waarop dit model is gebaseerd.
- Sessies & login levenscyclus — waarom login moet slagen voordat een antwoord betekenisvol is, en wanneer opnieuw in te loggen.
- Status & voorgrondafhandeling — het andere asynchrone kanaal (de event observer), apart van de hier beschreven interceptor.
- Probleemoplossing: sendBlocking blijft hangen tot timeout — de faalmodus die de laatste sectie van deze gids voorkomt.
-
API-referentie (
sdk-doc.zip) — de gezaghebbende, volledige lijst van subtypes voor elke operatie.