Skip to main content

SDK Integration

Overview

App to App Payment is a convenient way to pay for orders using smartphone. When user chooses to make payment for an order on merchant's mobile application using Zalopay Payment method, it will navigate to Zalopay application, and Zalopay will process the payment on purpose. This integration allows easy and fast payment, provides way to integrate promotions, reduces the need for physical contact or cash, but still ensures the security making payment between apps.

Payment Flow

In the next sections, we will guide you step by step to integrate Zalopay. You can visit our Github repository to refer our 4 example implementation (Android native app, iOS native app, React Native (Android/iOS) app, and Flutter (Android/iOS) app).

Prerequisites

Before you begin, make sure the following works are done for a smooth integration:

  • Registered merchant account successfully and obtained app_id, mac_key from Merchant Portal.
  • Understood the usage and specification of CreateOrder API and the concept of secure data transmission.
  • Your development environment meets below versions:
    • For iOS environment:
      • XCode version 14.1
      • Ruby version 2.7.6
      • Swift version 5.7.1
    • For Android environment:
      • Gradle version 7.5
      • Android Gradle plugin version 7.2.2
      • Minimum Gradle JDK version 11.0.13
  • Download zpdk-swift-x.x.x.framework (for iOS) and zpdk-release-vx.x.aar (for Android) here for integration with Zalopay application.

How it works

Here is how the payment flow works:

  1. Merchant adds Zalopay as a payment method in their mobile application, and also initialize ZPDK framework with provided app_id (ZPDK supports changing app_id for Merchants using multiple app ids).
  2. User chooses Zalopay payment method and then proceeds to make payment.
  3. Merchant's application calls CreateOrder API to create a new payment order. Merchant's app will receive zpTransToken in the response body.
  4. Merchant's app calls ZPDK framework's payOrder function, with zpTransToken received in step 3 as parameter.
  5. Merchant's app navigates to the order screen of Zalopay app. Note: merchant should handle the case when user has not installed Zalopay app yet.
  6. User completes the payment on Zalopay app. Zalopay app navigates back to merchant's app with response.
  7. Merchant's app handles the response and displays appropriate message to user.

In detail, our flow will look like this:

Integrations

iOS App

Step 1. Import and initialize Zalopay SDK

  1. Add framework ZPDK.framework to the project
  2. Change configuration to allow Zalopay to be initialized from the project, by adding zalo, Zalopay and Zalopay.api.v2 to the LSApplicationQueriesSchemes key. Also add merchant's application url scheme (merchant-deeplink in this example) to CFBundleURLSchemes key to support navigation between apps:
Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>zalo</string>
<string>Zalopay</string>
<string>Zalopay.api.v2</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>CFBundleURLSchemes</string>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.reactjs.native.example.demozpdk</string>
<key>CFBundleURLSchemes</key>
<array>
<string>merchant-deeplink</string>
</array>
</dict>
</array>
  1. In AppDelegate file, init ZPDK to handle the data exchange between Zalopay and the app:
AppDelegate.swift
//Call this function again whenever you want to reinitialize ZPDK to allow payment with another app_id
//The uriScheme is the same as merchant-deeplink configured in Info.plist above
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
ZalopaySDK.sharedInstance()?.initWithAppId(<appid>, uriScheme: "<merchant-deeplink>", environment: <ZPZPIEnvironment>)
return true
}

//Call ZPDK to handle the data exchange between Zalopay and the app. Call this function because ZPDK is currently checking whether the sourceApplication is Zalopay App or not.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return ZalopaySDK.sharedInstance().application(app, open: url, sourceApplication:"vn.com.vng.Zalopay", annotation: nil)
}

Step 2. Call Payment function

  1. Call CreateOrder API. Merchant should receive zpTransToken after calling API successfully
  2. Call Payment function using above zpTransToken:
PayOrder.swift
//Depend on where you handle ZPPaymentDelegate.
ZalopaySDK.sharedInstance()?.paymentDelegate = self

ZalopaySDK.sharedInstance()?.payOrder(zpTransToken.text)

Step 3. Handling returned results

ZPDK will navigate back to merchant's app with result. Merchant's app need to handle the returned result by using 3 following callbacks:

PayOrder.swift
func paymentDidSucceeded(_ transactionId: String!, zpTranstoken: String!, appTransId: String!) {
//Handle Success
}

func paymentDidCanceled(_ zpTranstoken: String!, appTransId: String!) {
//Handle User Canceled
}

func paymentDidError(_ errorCode: ZPPaymentErrorCode, zpTranstoken : String!, appTransId: String!) {
//Handle Error
}

Step 4. Handle case when user has not installed Zalopay

To handle the case when user has not installed Zalopay, in paymentDidError callback function, check whether the returned errorCode equals to ZPPaymentErrorCode.appNotInstall or not. If yes, call ZPDK's navigateToStore functions:

PayOrder.swift
func paymentDidError(_ errorCode: ZPPaymentErrorCode, zpTranstoken : String!, appTransId: String!) {
if (errorCode == .appNotInstall) {
ZalopaySDK.sharedInstance()?.navigateToZaloStore(); // navigator to Zalo App
ZalopaySDK.sharedInstance()?.navigateToZalopayStore(); // navigator to Zalopay App
return;
}
}

Android App

Step 1. Import and initialize Zalopay SDK

  1. In Android Studio, choose menu File -> New -> New module... -> Import .JAR/.AAR Package
  2. Select file zpdk-release-vx.x.aar, then name the new module
  3. Check whether you have imported the zpdk module to your project in the build.gradle file of app folder:
build.gradle (:app)
dependencies {
...
implementation(name:'zpdk-release-v3.1', ext:'aar')
}
  1. Add merchant's app url scheme in AndroidManifest.xml:
AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="merchant-deeplink"
android:host="app" />
</intent-filter>
  1. Initialize ZPDK in onCreate function of your desired activity:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
ZalopaySDK.init(<appID>, <Environment>);

//Reinit ZPDK if you want to pay with a different AppID
ZalopaySDK.tearDown();
ZalopaySDK.init(<appID>, Environment);
}

Step 2. Call Payment function

After calling CreateOrder API successfully and receiving zpTransToken, call Payment function with that zpTransToken:

PayOrder.kt
//Need to catch OnNewIntent event because Zalopay App will call deeplink to Merchant's app
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
ZalopaySDK.getInstance().onResult(intent)
}

ZalopaySDK.getInstance().payOrder(<Activity>, <Token>!!, "<MerchantApp Deeplink>", object: PayOrderListener {
...
})

Step 3. Handling returned results

Implement PayOrderListener interface and handle suitable logic code for your app in the functions corresponding to the transaction state: onPaymentSucceeded, onPaymentCanceled, onPaymentError:

PayOrder.kt
ZalopaySDK.getInstance().payOrder(..., object: PayOrderListener {
override fun onPaymentCanceled(zpTransToken: String?, appTransID: String?) {
//Handle logic when user cancels payment
}
override fun onPaymentError(ZalopayErrorCode: ZalopayError?, zpTransToken: String?, appTransID: String?) {
//Handle logic when payment error
}
override fun onPaymentSucceeded(transactionId: String, transToken: String, appTransID: String?) {
//Handle logic when payment successful
}
})

Step 4. Handle case when user has not installed Zalopay

To handle the case when user has not installed Zalopay, in onPaymentError function of PayOrderListener, check the returned ZalopayError = ZalopayError.ZALO_PAY_NOT_INSTALLED (code 1) and call ZPDK's navigate to store functions:

PayOrder.kt
if (ZalopayError == ZalopayError.PAYMENT_APP_NOT_FOUND) {
ZalopaySDK.getInstance().navigateToZaloOnStore(getApplicationContext())
ZalopaySDK.getInstance().navigateToZalopayOnStore(getApplicationContext())
}

React Native App

Step 1. Import and initialize Zalopay SDK

  1. For the integration and in initialization of ZPDK by React Native, we do the same as iOS and Android native app above
  2. Import neccessary Bridge classes at native sides to communicate effectively with React Native side

Step 2. Call Payment function

  1. At native sides, export a method for React Native to call ZPDK's payOrder method:
PayZaloBridge.m
RCT_EXPORT_METHOD(payOrder:
(NSString *)zpTransToken) {
[ZalopaySDK sharedInstance].paymentDelegate = self;
[[ZalopaySDK sharedInstance] payOrder:zpTransToken];
}
ZPModule.java
@ReactMethod
public void payOrder(String zpTransToken) {
Activity currentActivity = getCurrentActivity();
ZalopaySDK.getInstance().payOrder(currentActivity, zpTransToken, "demozpdk://app", payOrderListener);
}
  1. At React Native side, after calling CreateOrder API successfully and receiving zpTransToken, import PayZaloBridge native module and call its payOrder method:
PayOrder.js
import { NativeModules } from 'react-native';
const { PayZaloBridge } = NativeModules;

function payOrder() {
var payZP = NativeModules.PayZaloBridge;
payZP.payOrder(token);
}

Step 3. Handling returned results

  1. At iOS side, declare a native event for React Native to subscribe to. Then, with each callback is called, send that event to React Native side with appropriate data:
PayZaloBridge.m
- (NSArray<NSString *> *)supportedEvents
{
return @[@"EventPayZalo"];
}

- (void)paymentDidSucceeded:(NSString *)transactionId
zpTranstoken:(NSString *)zpTranstoken
appTransId:(NSString *)appTransId {
[self sendEventWithName:@"EventPayZalo" body:@{@"returnCode": [NSString stringWithFormat:@"%ld", (long)1], @"transactionId":transactionId ? transactionId : @"", @"zpTranstoken": zpTranstoken ? zpTranstoken : @"", @"appTransId": appTransId ? appTransId : @""}];
}

- (void)paymentDidCanceled:(NSString *)zpTranstoken
appTransId:(NSString *)appTransId {
[self sendEventWithName:@"EventPayZalo" body:@{@"returnCode": [NSString stringWithFormat:@"%ld", (long)4], @"zpTranstoken":zpTranstoken ? zpTranstoken : @"", @"appTransId": appTransId ? appTransId : @""}];
}

- (void)paymentDidError:(ZPPaymentErrorCode)errorCode
zpTranstoken:(NSString *)zpTranstoken
appTransId:(NSString *)appTransId {
[self sendEventWithName:@"EventPayZalo" body:@{@"returnCode": [NSString stringWithFormat:@"%ld", (long)errorCode], @"zpTranstoken":zpTranstoken ? zpTranstoken : @"", @"appTransId":appTransId ? appTransId : @""}];
}
  1. At Android side, declare a native event for React Native to subscribe to. Then, with each callback of payOrderListener is called, send that event to React Native side with appropriate data:
ZPModule.java
private ReactApplicationContext mReactContext;

private void sendEvent(ReactContext reactContext, String eventName, WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

PayOrderListener payOrderListener = new PayOrderListener() {
@Override
public void onPaymentSucceeded(String transactionId, String transToken, String appTransID) {
// Handle Success
WritableMap params = Arguments.createMap();
params.putString("transactionId", transactionId);
params.putString("transToken", transToken);
params.putString("appTransID", appTransID);
params.putString("returnCode", PAYMENTSUCCESS);
sendEvent(mReactContext, "EventPayZalo", params);
}

@Override
public void onPaymentCanceled(String transToken, String appTransID) {
// Handle Cancel
WritableMap params = Arguments.createMap();
params.putString("returnCode", PAYMENTCANCELED);
params.putString("zpTranstoken", transToken);
params.putString("appTransID", appTransID);
sendEvent(mReactContext, "EventPayZalo", params);
}

@Override
public void onPaymentError(ZalopayError ZalopayError, String transToken, String appTransID) {
// Handle Error
WritableMap params = Arguments.createMap();
params.putString("returnCode", PAYMENTFAILED);
params.putString("zpTranstoken", transToken);
params.putString("appTransID", appTransID);
sendEvent(mReactContext, "EventPayZalo", params);
}
};
  1. At React Native side, subscribe to the native event declared above and handle appropriately:
PayOrder.js
import { NativeModules, NativeEventEmitter } from 'react-native';
const { PayZaloBridge } = NativeModules;
const payZaloBridgeEmitter = new NativeEventEmitter(PayZaloBridge);

componentDidMount() {
this.subscription = payZaloBridgeEmitter.addListener(
'EventPayZalo',
(data) => {
if(data.returnCode === 1){
//Handle success case
} else{
//Handle other cases
}
}
);
}

componentWillUnmount() {
this.subscription.remove();
}

Step 4. Handle case when user has not installed Zalopay

We handle the same as Step 4 of iOS and Android native app above


Flutter App

Step 1. Import and initialize Zalopay SDK

For the integration and configuration of ZPDK, we do the same as Step 1 of iOS and Android native app above

Step 2. Call Payment function

  1. At iOS side, register Flutter Method Channel in function application used to init ZPDK iOS:
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...

let controller = window.rootViewController as? FlutterViewController
let nativeChannel = FlutterMethodChannel(name: "flutter.native/channelPayOrder",
binaryMessenger: controller!.binaryMessenger)
nativeChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == MethodNames.methodPayOrder else {
result(FlutterMethodNotImplemented)
return
}

let args = call.arguments as? [String: Any]
let _zptoken = args?["zptoken"] as? String

ZalopaySDK.sharedInstance()?.payOrder(_zptoken)
result("Processing...")
})
}
  1. At Android side, inherit MainActivity class to FlutterActivity class. Override method configureFlutterEngine to register Flutter method channel:
MainActivity.kt
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "flutter.native/channelPayOrder")
.setMethodCallHandler { call, result ->
if (call.method == "payOrder"){
val token = call.argument<String>("zptoken")
ZalopaySDK.getInstance().payOrder(this@MainActivity, token !!, "merchant-deeplink://app",object: PayOrderListener {
// ...
})
} else {
result.success("Method Not Implemented")
}
}
}
}
  1. At Flutter side, create Flutter MethodChannel. Then, after calling CreateOrder API successfully and receiving zpTransToken, call Payment function with that zpTransToken:
PayOrder.dart
static const MethodChannel platform = MethodChannel('flutter.native/channelPayOrder');
final String result = await platform.invokeMethod('payOrder', {"zptoken": zpToken});

Step 3. Handling returned results

  1. At iOS side, set handler to Flutter Event channel. Then, send payment result to Flutter by implementing functions relevant to each result code:
AppDelegate.swift
  //Init FlutterEventChannel to handle events
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...
let eventPayOrderChannel = FlutterEventChannel(name: "flutter.native/eventPayOrder",
binaryMessenger: controller!.binaryMessenger)
eventPayOrderChannel.setStreamHandler(self)
}

//Send event to Flutter via FlutterEventSink
private var eventSink: FlutterEventSink?
func paymentDidSucceeded(_ transactionId: String!, zpTranstoken: String!, appTransId: String!) {
guard let eventSink = eventSink else {
return
}
eventSink(["errorCode": 1, "zpTranstoken": zpTranstoken, "transactionId": transactionId, "appTransId": appTransId])
}

func paymentDidCanceled(_ zpTranstoken: String!, appTransId: String!) {
guard let eventSink = eventSink else {
return
}
eventSink(["errorCode": 4, "zpTranstoken": zpTranstoken, "appTransId": appTransId])
}

func paymentDidError(_ errorCode: ZPPaymentErrorCode, zpTranstoken: String!, appTransId: String!) {
guard let eventSink = eventSink else {
return
}
eventSink(["errorCode": errorCode, "zpTranstoken": zpTranstoken, "appTransId": appTransId])
}
  1. At Android side, set handler to Flutter Event channel. Then, send payment result to Flutter by implementing functions relevant to each result code:
MainActivity.kt
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
// ...

EventChannel(flutterEngine.dartExecutor.binaryMessenger, "flutter.native/eventPayOrder")
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, eventSink: EventChannel.EventSink) {
_eventSink = eventSink;
}
})

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelPayOrder)
.setMethodCallHandler { call, result ->
if (call.method == "payOrder") {
val tagSuccess = "[OnPaymentSucceeded]"
val tagError = "[onPaymentError]"
val tagCanel = "[onPaymentCancel]"
val token = call.argument<String>("zptoken")

ZalopaySDK.getInstance().payOrder(this@MainActivity, token !!, "merchant-deeplink://app",object: PayOrderListener {
override fun onPaymentCanceled(zpTransToken: String?, appTransID: String?) {
_eventSink?.success(mapOf(
"errorCode" to PAYMENTCANCELED, // code = 4
"zpTranstoken" to zpTransToken,
"appTransId" to appTransID,
))
}

override fun onPaymentError(ZalopayErrorCode: ZalopayError?, zpTransToken: String?, appTransID: String?) {
_eventSink?.success(mapOf(
"errorCode" to PAYMENTERROR,
"zpTranstoken" to zpTransToken,
"appTransId" to appTransID,
))
}

override fun onPaymentSucceeded(transactionId: String, transToken: String, appTransID: String?) {
_eventSink?.success(mapOf(
"errorCode" to PAYMENTCOMPLETE, // code = 1
"zpTranstoken" to transToken,
"transactionId" to transactionId,
"appTransId" to appTransID,
))
}
})
} else {
result.success("Method Not Implemented")
}
}
}
  1. At Flutter side, subscribe to the event channel created at native sides, then handle events relevant to the response code:
PayOrder.dart
const EventChannel eventChannel = EventChannel('flutter.native/eventPayOrder');

Future<void> subscribe() async {
eventChannel.receiveBroadcastStream().listen(
(data) {
var res = Map<String, dynamic>.from(data);
String message;
if (res["errorCode"] == 1) {
message = "Payment succees";
} else if (res["errorCode"] == 4) {
message = "User cancelled payment";
} else {
message = "Payment failed";
}
resultCallback(message);
},
onError: (error) {
resultCallback(error.toString());
},
);
}

Step 4. Handle case when user has not installed Zalopay

We handle the same as Step 4 of iOS and Android native app above


ZPDK's returned result data type

Case payment succeeded - Handle function paymentDidSucceeded()

FieldData TypeDescription
transactionIdstringThe id of current transaction
zpTranstokenstringThe token of current order
appTransIdstringThe app_trans_id of current transaction

Case payment cancelled - Handle function paymentDidCanceled()

FieldData TypeDescription
zpTranstokenstringThe token of current order
appTransIdstringThe app_trans_id of current transaction

Case payment cancelled by user - Handle function paymentDidError()

FieldData TypeDescription
errorCodestringCode of returned error, is one of the values: UNKNOWN, PAYMENT_APP_NOT_FOUND, INPUT_IS_INVALID, EMPTY_RESULT, FAIL
zpTranstokenstringThe token of current order
appTransIdstringThe app_trans_id of current transaction

See also

What's next

  • Finish coding? Verify your integration with testing.