This object exposes APIs to interact with the Pay side. This includes authentication, instrument handling and executing and listing payments.

Setup

The recommended way is to have a singleton instance of VipasoPay in your code and pass an observer (delegate/listener) instance that will receive all authentication state change events.

Configuration

When you create the instance, you can pass some configuration parameters (also Context on Android) to update the base path and the BLE settings. You can also pass a VipasoPayDelegate (iOS), VipasoPayListener (Android), instance, that will receive notifications when the authentication state changes.

iOS

VipasoPay(
    config: VipasoConfiguration(
        paymentBasePath: Config.basePath,
        bleServiceUUID: Config.bleServiceUUID,
        bleStatusUUID: Config.bleStatusUUID,
        bleWriteUUID: Config.bleWriteUUID
    ),
    delegate: delegate
)

Android

data class VipasoConfiguration(
    val paymentBasePath: String,
    val bleServiceUuid: String,
    val bleStatusUuid: String,
    val bleWriteUuid: String,
    val bleReadUUID: String
)

val vipasoConfiguration = VipasoConfiguration(
            paymentBasePath = "https://your.base.path",
            bleServiceUuid = "your service UUID",
            bleWriteUuid = "your Bluetooth Write UUID",
            bleStatusUuid = "your Bluetooth Status UUID",
            bleReadUUID = "your Bluetooth Read UUID"
        )
)

Once the parameters are ready, you can create VipasoPay instance anywhere. You can also use Dependency Injection to handle the instance creation and can be injected anywhere in your project, as follow :

// Instance Creation
val vipasoPay : VipasoPay = VipasoPayImpl(context = context, vipasoConfiguration = vipasoConfiguration, listener = listener)

// Instance Creation using Dependency Injection Dagger Hilt
@Module
@InstallIn(SingletonComponent::class)
class VipasoPayModule {

    @Provides
    @Singleton
    fun provideVipasoPay(
        @ApplicationContext context: Context,
        configuration: VipasoConfiguration,
        listener: VipasoPayListener?
    ): VipasoPay {
        return VipasoPayImpl(configuration, context, listener = listener)
    }

    @Provides
    fun provideVipasoConfig(@ApplicationContext context: Context): VipasoConfiguration {
        return VipasoConfiguration(
            paymentBasePath = "https://your.base.path",
            bleServiceUuid = "your service UUID",
            bleWriteUuid = "your Bluetooth Write UUID",
            bleStatusUuid = "your Bluetooth Status UUID",
            bleReadUUID = "your Bluetooth Read UUID"
        )
    }
}

Observing SDK events

By implementing the VipasoPayListener (Android) or VipasoPayDelegate (iOS) interface and passing it to the SDK, the client-side developer can subscribe to authentication state change events. You can pass the listener/delegate at init. Setting it automatically fires an event with the latest authentication state of the user. As the user signs up, logs in or out, an event will arrive and the app can be updated accordingly.

iOS example

extension RootCoordinator: VipasoPayDelegate {
    func onAuthenticationStateChange(vipaso: VipasoPayProtocol, authenticated: Bool) {
        if authenticated {
            displayMainCoordinator()
        } else {
            displayAuthenticationCoordinator()
        }
    }
}

Android example

override fun onAuthenticationStateChange(authenticated: Boolean) {
    isAuthenticated.value = authenticated
}

Authentication

Signup

The flow has three steps, each is an actual networking call.

Creating a new registration flow

First, you need to create a new registration flow with a phone number, this will send an OTP code to the phone number.

iOS

let request = StartSignupRequest(phoneNumber: phoneNumber)
vipasoPay.startSignupFlow(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.flowID)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val phoneNumber : String = "+4312345678"
val request = StartSignupRequest(phoneNumber)
        val response = vipasoPay.startSignupFlow(request)
        val data = response.data
        if (data != null) {
            print(data.flowId)
        } else {
            print(response.error?.message)
        }

Verify phone number

Next, you need to verify the phone number by sending the flow id and the OTP code you received to the backend.

iOS

let request = VerifyPhoneNumberRequest(flowID: flowID, otp: otp)
vipasoPay.verifyPhoneNumber(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.flowID)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val flowId : String = "flow Id"
val otp : String = "otp code"
val request = VerifyPhoneNumberRequest(flowId, otp)
        val response = vipasoPay.verifyPhoneNumber(request)
        val data = response.data
        if (data != null) {
            print(data.flowId)
        } else {
            print(response.error?.message)
        }

Completing signup flow

Lastly, you need to finish the signup request by sending all the user data you collected through the flow. The SDK will store your session token securely and the observer will be notified.

iOS

let request = FinishSignupRequest(
    flowID: flowID,
    password: password,
    email: email,
    firstName: firstName,
    lastName: lastName,
    phoneNumber: phoneNumber
)
vipasoPay.finishSignupFlow(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.flowID)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val flowId : String = "flowId"
val password : String = "password"
val email : String = "[email protected]"
val name : String = "John Doe"
val phoneNumber : String = "+431234567"

val request = FinishSignupRequest(flowId, password, email, name, phoneNumber)
        val response = vipasoPay.finishSignupFlow(request)
        val data = response.data
        if (data != null) {
            print(data.flowId)
        } else {
            print(response.error?.message)
        }

Login

Once you have an account, logging in is very straightforward. You pass the phone number as identifier with a password, the SDK will store your session token securely and the observer will be notified.

iOS

let request = LoginRequest(identifier: phoneNumber, password: password)
vipasoPay.login(request: request) { result in
    switch result {
    case .success:
        print("success")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val phoneNumber : String = "+431234567"
val password : String = "password"

val request = LoginRequest(phoneNumber, password)
        val response = vipasoPay.login(request)
        if (response.data != null) {
            print("success")
        } else {
            print(response.error?.message)
        }

Logging out

Simply calling the exposed logout function, the SDK will remove the stored session token and the observer will be notified.

PIN Recovery

Recovering the PIN has three steps:

  1. Use startRecoveryFlow with a phone number to receive a flow id first.
func startRecoveryFlow(
    request: StartRecoveryRequest,
    completion: @escaping (Result<StartRecoveryResponse, VipasoError>) -> Void
)
  1. This will send an OTP code to the phone number you set. Send the code and the flow id with sendRecoveryOTP to the backend to receive a settings flow id and csrf token.
func sendRecoveryOTP(
    request: RecoveryOTPRequest,
    completion: @escaping (Result<RecoveryOTPResponse, VipasoError>) -> Void
)
  1. With the settings flow id, csrf token and the new PIN the user selected, you can update the PIN on the backend with finishRecoveryFlow and you are done.
func finishRecoveryFlow(
    request: FinishRecoveryRequest,
    completion: @escaping (Result<FinishRecoveryResponse, VipasoError>) -> Void
)

User data

Fetching user info

To get all info about the customer, use fetchUser.

func fetchUser(completion: @escaping (Result<FetchUserResponse, VipasoError>) -> Void)

This will return a user object with the following data.

public struct VipasoUser {
    public let id: String?
    public let phone: String?
    public let name: String?
    public let email: String?
}

Instruments

Onboarding a new instrument

To add a new instrument, you can use the exposed startOnboardingInstrument api. The SDK will return a url that you then need to display in a web view so that users can fill their card information.

iOS

Hint: WKWebView provides a faster experience then UIWebView. By implementing WKNavigationDelegate, you receive all navigation events so you can also dismiss the web view at the end of the flow automatically.

let request = StartOnboardingInstrumentRequest(
    instrumentType: .card,
    lang: .en
)
vipasoPay.startOnboardingInstrument(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.onboardingURL)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val lang : String = "EN"
val instrumentType : String = "CARD"

val request = StartOnboardingInstrumentRequest(instrumentType, lang)
        val response = vipasoPay.startOnboardingInstrument(request)
        if (response.data != null) {
            print (response.data.onboardingURL)
        } else {
            print(response.error?.message)
        }

List instruments

Listing your instruments is a simple fetch call without any parameters.

iOS

vipasoPay.fetchInstruments { result in
    switch result {
    case .success(let response):
        print("success: \(response.instruments)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val response = vipasoPay.fetchInstruments()
        if (response.data != null) {
            print (response.data?.instrumentList)
        } else {
            print(response.error?.message)
        }

Creating a new MNO instrument

You can create a mobile money instrument with the following api.

iOS

let request = CreateMNOInstrumentRequest(
  phone: phoneNumber,
  customerName: name,
  deviceName: deviceName,
  lang: .en
)
vipasoPay.createMNOInstrument(request: request) { result in
  switch result {
    case .success(let response):
    promise(.success(response.id))
    case .failure(let error):
    promise(.failure(.generic(error: error)))
  }
}

Android

val request = CreateMnoInstrumentRequest(
  phone = phoneNumber,
  customerName = name,
  deviceName = deviceName,
  lang = VipasoLang.from(Locale.getDefault())
)
val response = vipasoWrapper.vipasoPay?.createMnoInstrument(request = request)
return if (response?.data != null) {
  val id = response.let { it.data?.id }
  VipasoResponseWrapper(vipasoResponse = VipasoResponse((id)))
} else {
  VipasoResponseWrapper(VipasoResponse(error = response?.error))
}

Creating an instrument with a third-party id

You can create an instrument with an existing third-party id from your own system.

iOS

let request = CreateThirdPartyInstrumentRequest(
  thirdPartyID: "<instrument-id>",
  instrumentType: .card,
  lang: .en
)
vipasoPay.createThirdPartyInstrument { result in
  switch result {
    case .success(let response):
    // handle success
    case .failure(let error):
    // handle error
  }
}

Android

vipasoPay.createThirdPartyInstrument(
    CreateThirdPartyInstrumentRequest(
        thirdPartyId = "<instrument-id>",
        instrumentType = VipasoInstrumentType.MNO,
        lang = VipasoLang.EN
    )
)

Withdrawal

Withdrawal allows the funds to be transferred to a supplied phone number. The response contains an id and a status. The possible values of the status areinitiated, inProgress, completed, failed, cancelled.

iOS

let request = WithdrawalRequest(
	instrumentID: instrumentID,
	amount: amount,
	currency: currency,
	phone: phoneNumber
)
vipasoPay.withdrawal(request: request) { result in
	switch result {
		case .success(let response):
			promise(.success(response))
    case .failure(let error):
    	promise(.failure(.generic(error: error)))
	}
}

Android

Payments

List transactions

Listing your payment transaction history is a simple fetch call without any parameters.

iOS

vipasoPay.fetchPayments(page: 3, status: .completed) { result in
    switch result {
    case .success(let response):
        print("success: \(response.payments)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val response = vipasoPay.fetchPayments(page: 3, status: VipasoPaymentPagaStatus.COMPLETED)
        if (response.data != null) {
            print(response.data?.payments)
        } else {
           print(response.error?.message)
        }

Register for BLE payment requests

The flow has two steps on iOS. You need to connect to the PoS side via BLE and register as a payment receiver to be notified about any payment requests coming from PoS.

On Android you start bluetooth scan and receive a completable payment response that includes data on payment and a call to notify server on payment result.

Connecting to PoS via BLE

iOS

vipasoPay.connectBLE { [weak self] result in
    switch result {
    case .success(let response):
        // Start listening for payments after the connection succeeds
        self?.vipasoPay.setupBLEPaymentReceiver { result in
            ...
        }
        print("success: \(response)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val response = vipasoPay.startBLE()
        if (response.data != null) {
            print(response)
            //inform payment that payment was accepted/rejected
            response.data.onComplete(true)
        } else {
           print(response.error?.message)
        }

Registering as receiver to incoming payment requests

This can be called after when the BLE connection has been established.

iOS

vipasoPay.setupBLEPaymentReceiver { result in
    switch result {
    case .success(let response):
        print("success: \(response)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

N/A

Accepting a payment

When an incoming payment request arrives to the BLE receiver, users can decide whether to execute it or not. There are two things to do when accepting it.

Executing the payment and actually paying

iOS

let request = ExecutePaymentRequest(
    paymentID: paymentID,
    instrumentID: instrumentID,
    invoiceAmendments: nil,
    lang: .en
)
vipasoPay.executePayment(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.payment)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val request = ExecutePaymentRequest(
                    paymentID: "paymentId",
                    instrumentID: "instrumentID",
                    lang: "EN",
                    confirmationTimestamp: "timeStamp now"
                )
val response = vipasoPay.executePayment(request)
if (response.data != null) {
            print(response.payment)
        } else {
           print(response.error?.message)
        }

Notifying PoS about the accepted payment

To notify PoS about an accepted payment, call confirmBLEPayment with a true status.

iOS

vipasoPay.confirmBLEPayment(payment: paymentID, status: true) { result in
    switch result {
    case .success(let response):
        print("success: \(response)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

vipasoPay.sendBLEResponse(paymentID, true)

Rejecting a payment

To notify PoS about a rejected payment, it's also important to call confirmBLEPayment with a false status.

iOS

vipasoPay.confirmBLEPayment(payment: paymentID, status: false) { result in
    switch result {
    case .success(let response):
        print("success: \(response)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

vipasoPay.sendBLEResponse(paymentID, false)

Fetching payment details

Fetching the details of a payment is a simple fetch call, you only need to set the payment id.

iOS

let request = FetchPaymentDetailsRequest(paymentID: paymentID)
vipasoPay.fetchPaymentDetails(request: request) { result in
    switch result {
    case .success(let response):
        print("success: \(response.payment)")
    case .failure(let error):
        print("error: \(error.localizedDescription)")
    }
}

Android

val paymentId : String = "paymentId"
val request = FetchPaymentDetailsRequest(paymentId)
        val response = vipasoPay.fetchPaymentDetails(request)
        return if (response.data != null) {
           print(response.payment)
        } else {
            print(response.error?.message)
        }

Offline Payments

Syncing Offline Payments

Registering for incoming Offline Payments

Starts listening for incoming Offline Payment Requests

iOS

vipasoPay.startReceivingOfflinePaymentsViaBle { result in
		switch result {
    	case .receivedPayment(let paymentRequest):
      		print("success: \(paymentRequest)")          
     	case .finishedPayment, .error, .cancelled, .otherPaid:
      		print("completion success/error")  
    }
}

Responds in a VipasoPaymentResultenum

public enum VipasoPaymentResult {  
    case receivedPayment(VipasoOfflinePaymentRequest)  
    case finishedPayment(VipasoPayOfflinePaymentResponse)  
    case error(VipasoError)  
    case cancelled  
    case otherPaid  
}

public struct VipasoOfflinePaymentRequest: Codable, Equatable {
    public var paymentID: String
    public var merchantID: String
    public var operatorID: String
    public var amount: String
    public var currency: String
    public var createdAt: String
}

public struct VipasoPayOfflinePaymentResponse: Codable, Equatable {
    public var paymentID: String
    public var amount: String
    public var tip: String
    public var currency: String
    public var preAuthJWS: String
    public var createdAt: String
}

Android



Accepting Offline Payments

Sends the acceptance of an incoming Offline Payment request Returns true if the payment was concluded.

NOTE: The successfull end of the payment is indicated by the startReceivingOfflinePaymentsViaBlemethod callback with the finishedPayment(VipasoPayOfflinePaymentResponse)enum case.

iOS

let response = VipasoPayOfflinePaymentResponse(
	paymentID: paymentIDString,
  amount: amountString,
  tip: tipString,
  currency: currencyString,
  preAuthJWS: preAuthorizationString,
  createdAt: createdAdDate.iso8601String
) 
vipasoPay.acceptOfflinePayment(response: response) { result in
     switch result {
       case .success(let response):
	       print("success: \(response)")          
       case .failure(let error):
  	     print("completion success/error")  
     }
  }

Incoming parameter is VipasoPayOfflinePaymentResponse

public struct VipasoPayOfflinePaymentResponse: Codable, Equatable {
    public var paymentID: String
    public var amount: String
    public var tip: String
    public var currency: String
    public var preAuthJWS: String
    public var createdAt: String
}

Android



Cancelling Offline Payments

Cancels an Offline Payment. Returns true if the payment was successfully cancelled.

iOS

vipasoPay.cancelOfflinePayment { result in  
		switch result {
			case .success(let response):
				print("success: \(response)")          
			case .failure(let error):
				print("completion success/error")  
		}
}

Android



Stopping Offline payment BLE

Disconnects the offline BLE stack.

iOS

vipasoPay.stopOfflineBLE()

Android



Sync Offline Payments

Synchronizes the Offline Payments stored in the mobile app.

iOS

vipasoPay.syncPayments { result in  
		switch result {  
			case .success(let response):  
				print("success: \(response)")    
			case .failure(let error):  
				print("completion success/error")   
		}  
}

NOTE: Should be called when the device comes online. The iOS SDK does not do synchronization unless this method is called.

Android



Feature flags

Fetching feature flags

We support adding custom feature flags so you can remote control what your users have access to in their apps. The SDK fetches them under the hood and exposes an API to read them on the client-side. The API prefers using cached flags from the database.

iOS

/// Fetches available feature flags
/// - Parameter completion: Fetching feature flags completion handler
func fetchFeatureFlags(
    completion: @escaping (Result<FetchFeatureFlagsResponse, VipasoError>) -> Void
)

Android

suspend fun fetchFeatureFlags(): VipasoResponse<FetchFeatureFlagsResponse>

What’s Next