Integrazione App-to-App

1. Introduzione & Panoramica

1.1 Cos'è l'Integrazione App-to-App?

L'integrazione App-to-App è la soluzione che consente alla tua applicazione (il Registratore di Cassa Elettronico o il sistema POS) di comunicare in modo sicuro con l'Applicazione di Pagamento Market Pay sullo stesso dispositivo.

Ciò viene realizzato utilizzando la Comunicazione Inter-Processo (IPC). Nell'ambiente Android, l'IPC consente la comunicazione tra due applicazioni separate che girano contemporaneamente sullo stesso dispositivo.

1.2 Perché utilizzare il Market Pay SDK?

Integrare il Market Pay SDK è il modo più semplice per stabilire la comunicazione App-to-App. Lo scopo principale dell'SDK è quello di astrarre i dettagli complessi e di basso livello dell'IPC Android, come AIDL (Android Interface Definition Language) e la connettività Binder.

Invece di gestire manualmente il marshalling dei dati grezzi e il binding dei processi, l'SDK fornisce una chiara interfaccia Client (Plain Old Java Object o POJO). Questa interfaccia permette ai tuoi sviluppatori di avviare azioni come pagamenti e login utilizzando semplici e intuitive chiamate di metodo, come sendPaymentRequest().

L'SDK include anche funzionalità robuste per la gestione dei messaggi e dello stato:

  • Conversione Interprotocollo: Converte automaticamente i messaggi tra diversi standard di protocollo (es. Nexo Retailer Protocol) quando necessario.
  • Gestione della Connettività: Gestisce la connessione, disconnessione e riconnessione al servizio dell'Applicazione di Pagamento.
  • Feedback Asincrono: Utilizza un Interceptor per consegnare le risposte delle transazioni e un EventObserver per notificare alla tua applicazione i cambiamenti di stato (es. IdleTransactionState, PaymentTransactionState).

1.3 Checklist dei Prerequisiti

Prima di iniziare la configurazione dell'SDK, assicurati che i seguenti requisiti siano soddisfatti:

RequisitoValore / AzioneNote
Android Min SDK22Richiesto per l'applicazione che integra l'SDK.
Android Target SDK30Richiesto per l'applicazione che integra l'SDK.
File di Dipendenza SDKFile .aar fornito (es. client-clientRelease-X.X.X.aar)Deve essere posizionato nella cartella libs del tuo modulo.

2. Configurazione: Installazione e Inizializzazione dell'SDK

Puoi trovare una descrizione dettagliata e un esempio d'uso in: pl.novelpay.client.sdk come esempio di configurazione SDK.

2.1 Passo 1: Aggiungi la Dipendenza SDK

  1. Posiziona il file .aar fornito (es. client-clientRelease-0.0.8_cc9d51b.aar) nella cartella libs del tuo modulo.
  2. Aggiungi la riga di implementazione al file build.gradle a livello di modulo:
dependencies {
    // ... altre dipendenze
    implementation files('libs/client-clientRelease-0.X.X.aar')
}

2.2 Passo 2: Configura e Costruisci il Client

La ClientFactory viene utilizzata una sola volta, tipicamente all'avvio della tua applicazione (es. nella classe Application o nell'Activity principale), per stabilire la connessione e configurare gli handler.

Questo è il blocco di codice più critico. Suddividilo in passaggi chiari e commentati:

  1. Avvia la catena di configurazione
  2. Configura l'app di destinazione (configurazione SDK)
  3. Configura il protocollo (nel nostro caso esempio Nexo Retailer Protocol v3)
  4. Imposta l'handler di risposta ai messaggi
  5. Imposta l'handler degli eventi applicativi
  6. Finalizza e connetti

 

// 1. Avvia la catena di configurazione
val clientInstance = ClientFactory
    .bindConfiguration( 
        // 2. CONFIGURA APP DI DESTINAZIONE (The Payment Terminal App)
        SDKConfiguration.defaultConfiguration(
            clientApplicationInfo = PackageInfo(
                applicationName = applicationInfo.name, // Nome della tua app 
                applicationPackage = packageName // Nome del package della tua app
            )
        )
    )
    .bindContext(context = applicationContext)
    .bindProtocolConfiguration(
        // 3. CONFIGURA PROTOCOLLO (esempio Nexo Retailer Protocol v3. 
        // I parametri della tua applicazione come POS possono essere specificati qui. 
        RetailerConfiguration(saleID = "Communicator_SALE")
    )
    .bindInterceptor { message: DomainMessage -> 
        // 4. IMPOSTA HANDLER RISPOSTA MESSAGGI (Vedi Sezione 3.2)
        // Qui arrivano tutte le risposte delle transazioni (Successo, Errore, ecc.)
        when (message) {
        
            is SuccessRetailerLoginResponse -> { /* Gestisci login avvenuto con successo */ }
            is ErrorRetailerLoginResponse -> { /* Gestisci login fallito */ }
            // ... tutte le altre risposte
        }
    }
    .bindEventObserver { event: ObservableEvent -> 
        // 5. IMPOSTA HANDLER EVENTI APPLICAZIONE (Vedi Sezione 3.3)
        // Gestisce cambiamenti di stato (Idle, PaymentProcessing) e foregrounding.
        when (event) {
            is ObservableEvent.TransactionStateChanged -> { 
                // Gestisci cambiamenti di stato e porta la tua app in primo piano
            }
            is ObservableEvent.ServerEvent -> { 
                // Gestisci eventi generali del server
            }
        }
    }
    .build() // 6. Finalizza e connetti

3. Utilizzo Principale: Invio Richieste e Gestione Risposte

3.1 Specifiche del protocollo Nexo Retailer

Ogni sessione di comunicazione dovrebbe iniziare con una RetailerLoginRequest. Questo dovrebbe essere attivato nei seguenti casi:

  • Aggiornamento dell'Applicazione di Pagamento
  • Riavvio dell'Applicazione di Pagamento
  • Riavvio dell'applicazione client (la tua)
  • La connessione tra le applicazioni è stata persa
  • È stata inviata una RetailerLogoutRequest

Una nuova procedura di Login deve essere attivata per istanziare la sessione. Altrimenti le richieste dalla tua applicazione saranno considerate non autorizzate e verranno ignorate dall'Applicazione di Pagamento.

3.1 Invio Richieste (Interfaccia Client)

L'istanza dell'interfaccia Client restituita da ClientFactory::build() è il tuo principale strumento di comunicazione. Tutti i metodi sono funzioni sospese e dovrebbero essere chiamati da uno scope delle coroutine.

AzioneMetodoNote
Avvia Sessionesuspend fun sendLoginRequest()Deve essere chiamato per primo. Avvia una nuova sessione dopo ogni interruzione (riavvii, aggiornamenti, perdita di connessione).
Pagamentosuspend fun sendPaymentRequest(...)Utilizzato per Vendite, Rimborsi, Pre-Autorizzazioni e Completamenti.
Cancellazionesuspend fun sendReversalRequest(...)Utilizzato per la cancellazione delle transazioni.
Verifica Statosuspend fun sendTransactionStatusRequest(...)Recupera lo stato dell'ultima transazione completata.

Richiesta di Login

Ogni integrazione deve iniziare stabilendo una sessione sicura. La richiesta di login deve essere chiamata all'avvio dell'applicazione e riattivata se la connessione viene persa o l'app terminale viene riavviata.

// Stabilire una sessione di comunicazione sicura
fun performLogin() {
    viewModelScope.launch {
        try {
            client.sendLoginRequest()
            // Il successo viene gestito tramite il tuo Interceptor registrato
        } catch (e: Exception) {
            Logger.e("Login failed to initiate: ${e.message}")
        }
    }
}

Iniziare un Pagamento

Questa è la funzione principale per l'elaborazione delle transazioni finanziarie. Richiede un ID transazione univoco per il tracciamento e gli importi specifici da addebitare.

// Avviare una transazione di pagamento standard
fun startPayment(amount: Double) {
    viewModelScope.launch {
        client.sendPaymentRequest(
            transactionID = "Txn_${UUID.randomUUID()}", // ID vendita univoco
            paymentAmounts = PaymentAmounts(amount, "EUR"), 
            paymentType = PaymentType.Normal
        )
    }
// Le risposte saranno elaborate in modo asincrono nell'Interceptor (Sezione 3.2).

TransactionID deve essere lungo massimo 35 caratteri. Se usi UUID, rimuovi il "-" (trattino) dall'UUID generato.

Richiesta Stato Transazione

Usa questa funzione per verificare l'esito finale di una transazione se la tua app ha perso la connessione durante l'elaborazione. Recupera lo stato dell'ultimo messaggio completato.

// Verificare lo stato di un riferimento di transazione specifico
fun checkStatus(reference: MessageReference) {
    viewModelScope.launch {
        client.sendTransactionStatusRequest(reference)
    }
}

Richiesta di Storno

Se una transazione è stata completata per errore (es. importo errato inserito), la richiesta di storno annulla quel record specifico usando l'ID transazione terminale originale.

// Annullare una transazione precedentemente completata
fun reverseTransaction(originalPoiId: String) {
    viewModelScope.launch {
        client.sendReversalRequest(
            saleReferenceId = "Sale_Ref_98765",
            poiTransactionId = originalPoiId, // L'ID restituito dal terminale nella risposta originale
            transactionData = ReversalRequestMessageArguments.TransactionAmountsData(...)
        )
    }
}

Aggiornamento

Questa funzione è dedicata ad attività amministrative del terminale, come avviare aggiornamenti software o scaricare i parametri più recenti disponibili.

// Avviare attività amministrative come aggiornamenti del terminale
fun updateTerminal() {
    viewModelScope.launch {
        client.sendAdminRequest(RetailerAdminExtension.Update)
    }
}

Diagnosi

Un semplice controllo "heartbeat" per verificare che il collegamento tra la tua app e il terminale di pagamento sia ancora attivo senza avviare una transazione completa.

// Verificare la connessione heartbeat al terminale
fun testConnection() {
    viewModelScope.launch {
        client.sendTestConnectionRequest(
            DiagnosisRequestMessageArguments(messageId = "test_123")
        )
    }
}

Richiesta di Logout

 

// Chiudere la sessione corrente in modo sicuro
fun performLogout() {
    viewModelScope.launch {
        client.sendLogoutRequest()
    }
}

 

3.2 Ricezione Risposte (L'Interceptor)

Tutte le risposte alle richieste inviate tramite l'interfaccia Client vengono consegnate all'Interceptor che hai definito nella configurazione.

Il parametro in ingresso ha tipo generico DomainMessage; tutti i messaggi nella comunicazione App-to-App sono sottotipi di questa classe. Si consiglia un controllo del tipo per specificare la strategia di gestione per ciascun tipo di risposta. 

Ogni operazione come (Pagamento, Storno, Rimborso...) ha il proprio tipo di messaggio. Può essere trovato nel pacchetto di documentazione retailer-protocol. 

Mappa delle richieste e risposte per le principali operazioni:

OperazioneTipo RichiestaTipo RispostaSottotipi Risposta Attesi
LoginRetailerLoginRequestRetailerLoginResponseSuccessRetailerLoginResponse, FailedRetailerLoginResponse
PagamentoRetailerPaymentRequest

RetailerPaymentResponse

RetailerDisplayRequest

SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse
AnnullamentoRetailerAbortRequestRetailerAbortResponseErrorRetailerPaymentResponse
StornoRetailerReversalRequestRetailerReversalResponseSuccessRetailerReversalResponse, ErrorRetailerReversalResponse
StatoTransactionStatus

RetailerTransactionStatusResponse

RetailerDisplayRequest

SuccessRetailerTransactionStatusResponse, ErrorRetailerTransactionStatusResponse, RetailerDisplayRequest

Nota. La richiesta di pagamento viene utilizzata anche per Rimborsi, Pre-Autorizzazioni e Completamenti di Pre-Autorizzazione. Non esistono sottotipi separati per queste operazioni. DisplayRequest notificherà durante l'elaborazione quale operazione è in corso.

4. Gestione Stati ed Eventi

4.1 L'EventObserver (Cambiamenti di Stato)

Usa il bindEventObserver per monitorare lo stato operativo dell'Applicazione di Pagamento e gestire l'interfaccia utente della tua applicazione.

ObservableEvent.ServerEvent questo evento segnala che un'azione interna specifica o una notifica si è verificata all'interno dell'Applicazione di Pagamento.

L'evento chiave è ObservableEvent.TransactionStateChanged.

StatoDescrizioneRaccomandazione Foreground
PaymentTransactionStateL'elaborazione del pagamento è iniziata.Opzionale: Porta la tua applicazione in primo piano (es. per mostrare una UI personalizzata durante l'autorizzazione).
IdleTransactionStateL'Applicazione di Pagamento ha terminato la transazione.Raccomandato: Porta la tua applicazione in primo piano per mostrare il risultato finale.
bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is IdleTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

4.2 BringToForeground

bringToForeground(MainActivity :class) - è una funzione responsabile per "portare" in primo piano la tua applicazione dopo cambiamenti di stato nell'Applicazione di Pagamento. MainActivity:class è un segnaposto per la activity attualmente in esecuzione della tua applicazione. Se la tua applicazione è Single-Activity non devi preoccuparti delle transizioni tra activity, quindi basta fornire la tua Main Activity come nell'esempio.

  • PaymentTransactionSate -> Se attiva/presente: torna alla tua applicazione durante l'autorizzazione del pagamento.
  • IdleTransactionState -> Se attiva/presente: torna alla tua applicazione alla fine del pagamento.

Ad esempio: vuoi mostrare una pubblicità nella tua applicazione o dettagli personalizzati della transazione durante l'elaborazione del pagamento invece dello spinner dell'Applicazione di Pagamento. Tutto ciò che devi fare per ottenerlo: specificare il percorso per PaymentTransactionState.


illustrazione:

.bindEventObserver { event: ObservableEvent -> 
    when (event) {
        is ObservableEvent.TransactionStateChanged -> when (event.state) {
            // Porta la tua app in primo piano quando il pagamento termina
            is IdleTransactionState -> bringToForeground(MainActivity::class.java) 
            // Opzionale: Porta la tua app in primo piano quando il pagamento inizia
            is PaymentTransactionState -> bringToForeground(MainActivity::class.java)
            else -> { /* Ignora altri stati */ } 
        }
        is ObservableEvent.ServerEvent -> Logger.d("New server event") 
    } 
}

Esempio per portare la tua app in primo piano quando l'autorizzazione è in corso:

bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is PaymentTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

Esempio per portare la tua app in primo piano quando il pagamento è completato:

bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is IdleTransactionState
bringToForeground(MainActivity :class
is PaymentTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

5. Appendice: Regole ProGuard

Se il tuo progetto utilizza l'offuscamento tramite R8/ProGuard, assicurati di aggiungere le seguenti regole al tuo file proguard-rules.pro.

 -keep class pl.novelpay.** { *; } 
 -dontwarn pl.novelpay.** 
 -keep class org.koin.** { *; } 
 -keep class androidx.lifecycle.** { *; } 
 -keep class com.jakewharton.threetenabp.** { *; } 
 -dontwarn com.google.gson.annotations.SerializedName  -dontwarn javax.xml.stream.Location 
 -dontwarn javax.xml.stream.XMLEventReader 
 -dontwarn javax.xml.stream.XMLInputFactory 
 -dontwarn javax.xml.stream.events.Attribute 
 -dontwarn javax.xml.stream.events.Characters 
 -dontwarn javax.xml.stream.events.StartElement 
 -dontwarn javax.xml.stream.events.XMLEvent 
 -dontwarn kotlinx.parcelize.Parcelize 
 -keepattributes
InnerClasses,Signature,RuntimeVisible*Annotations,EnclosingMethod  -dontwarn com.google.gson.TypeAdapter 
 -dontwarn com.google.gson.stream.JsonReader  -dontwarn com.google.gson.stream.JsonWriter  -keep class kotlin.Metadata { *; } 
 -keep class org.simpleframework.** { *; } 
 # Preserve all annotations. 
 -keepattributes Annotation 

6. Allegati

Correlato a