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.
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
- For iOS environment:
- Download
zpdk-swift-x.x.x.framework
(for iOS) andzpdk-release-vx.x.aar
(for Android) here for integration with Zalopay application.
How it works
Here is how the payment flow works:
- Merchant adds Zalopay as a payment method in their mobile application, and also initialize ZPDK framework with provided
app_id
(ZPDK supports changingapp_id
for Merchants using multiple app ids). - User chooses Zalopay payment method and then proceeds to make payment.
- Merchant's application calls CreateOrder API to create a new payment order. Merchant's app will receive
zpTransToken
in the response body. - Merchant's app calls ZPDK framework's
payOrder
function, withzpTransToken
received in step 3 as parameter. - 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.
- User completes the payment on Zalopay app. Zalopay app navigates back to merchant's app with response.
- 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
- Add framework
ZPDK.framework
to the project - 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:
<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>
- In AppDelegate file, init ZPDK to handle the data exchange between Zalopay and the app:
- Swift
- Objective C
//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)
}
//Call this function again whenever you want to reinitialize ZPDK to allow payment with another app_id
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[ZalopaySDK sharedInstance] initWithAppId:<appID> uriScheme:@"<uriScheme>" environment:<ZPZPIEnvironment>]; //Init ZPDK
}
//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.
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [[ZalopaySDK sharedInstance] application:app openURL:url sourceApplication:@"vn.com.vng.Zalopay" annotation:nil];
}
Step 2. Call Payment function
- Call CreateOrder API. Merchant should receive
zpTransToken
after calling API successfully - Call Payment function using above
zpTransToken
:
- Swift
- Objective C
//Depend on where you handle ZPPaymentDelegate.
ZalopaySDK.sharedInstance()?.paymentDelegate = self
ZalopaySDK.sharedInstance()?.payOrder(zpTransToken.text)
//Depend on where you handle ZPPaymentDelegate.
[ZalopaySDK sharedInstance].paymentDelegate = self;
[[ZalopaySDK sharedInstance] payOrder:zpTransToken];
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:
- Swift
- Objective C
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
}
- (void)paymentDidSucceeded:(NSString *)transactionId zpTranstoken:(NSString *)zpTranstoken appTransId:(NSString *)appTransId {
//Handle Success
}
- (void)paymentDidCanceled:(NSString *)zpTranstoken appTransId:(NSString *)appTransId {
//Handle User Canceled
}
- (void)paymentDidError:(ZPPaymentErrorCode)errorCode zpTranstoken:(NSString *)zpTranstoken appTransId:(NSString *)appTransId {
//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:
- Swift
- Objective C
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;
}
}
- (void)paymentDidError:(ZPPaymentErrorCode)errorCode zpTranstoken:(NSString *)zpTranstoken appTransId:(NSString *)appTransId {
if (errorCode == ZPPaymentErrorCode.appNotInstall) {
[[ZalopaySDK sharedInstance] navigateToZaloStore];
[[ZalopaySDK sharedInstance] navigateToZalopayStore];
return;
}
Android App
Step 1. Import and initialize Zalopay SDK
- In Android Studio, choose menu File -> New -> New module... -> Import .JAR/.AAR Package
- Select file zpdk-release-vx.x.aar, then name the new module
- Check whether you have imported the
zpdk
module to your project in the build.gradle file of app folder:
dependencies {
...
implementation(name:'zpdk-release-v3.1', ext:'aar')
}
- Add merchant's app url scheme in
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>
- Initialize ZPDK in
onCreate
function of your desired activity:
- Kotlin
- Java
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);
}
@Override
public void onCreate() {
...
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
:
- Kotlin
- Java
//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 {
...
})
//Need to catch OnNewIntent event because Zalopay App will call deeplink to Merchant's app
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
ZalopaySDK.getInstance().onResult(intent);
}
ZalopaySDK.getInstance().payOrder(
<Activity>, <Token>, <YourAppUriScheme>, new MyZalopayListener()
);
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
:
- Kotlin
- Java
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
}
})
private static class MyZalopayListener implements PayOrderListener {
@Override
public void onPaymentSucceeded(final String transactionId, final String transToken, final String appTransID) {
//Handle logic when payment successful
}
@Override
public void onPaymentCanceled(String zpTransToken, String appTransID) {
//Handle logic when user cancels payment
}
@Override
public void onPaymentError(ZalopayError ZalopayError, String zpTransToken, String appTransID) {
//Handle logic when payment error
}
}
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:
- Kotlin
- Java
if (ZalopayError == ZalopayError.PAYMENT_APP_NOT_FOUND) {
ZalopaySDK.getInstance().navigateToZaloOnStore(getApplicationContext())
ZalopaySDK.getInstance().navigateToZalopayOnStore(getApplicationContext())
}
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
- For the integration and in initialization of ZPDK by React Native, we do the same as iOS and Android native app above
- Import neccessary Bridge classes at native sides to communicate effectively with React Native side
Step 2. Call Payment function
- At native sides, export a method for React Native to call ZPDK's
payOrder
method:
RCT_EXPORT_METHOD(payOrder:
(NSString *)zpTransToken) {
[ZalopaySDK sharedInstance].paymentDelegate = self;
[[ZalopaySDK sharedInstance] payOrder:zpTransToken];
}
@ReactMethod
public void payOrder(String zpTransToken) {
Activity currentActivity = getCurrentActivity();
ZalopaySDK.getInstance().payOrder(currentActivity, zpTransToken, "demozpdk://app", payOrderListener);
}
- At React Native side, after calling CreateOrder API successfully and receiving
zpTransToken
, importPayZaloBridge
native module and call itspayOrder
method:
import { NativeModules } from 'react-native';
const { PayZaloBridge } = NativeModules;
function payOrder() {
var payZP = NativeModules.PayZaloBridge;
payZP.payOrder(token);
}
Step 3. Handling returned results
- 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:
- (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 : @""}];
}
- 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:
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);
}
};
- At React Native side, subscribe to the native event declared above and handle appropriately:
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
- At iOS side, register Flutter Method Channel in function
application
used to init ZPDK iOS:
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...")
})
}
- At Android side, inherit
MainActivity
class toFlutterActivity
class. Override methodconfigureFlutterEngine
to register Flutter method channel:
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")
}
}
}
}
- At Flutter side, create Flutter MethodChannel. Then, after calling CreateOrder API successfully and receiving
zpTransToken
, call Payment function with thatzpTransToken
:
static const MethodChannel platform = MethodChannel('flutter.native/channelPayOrder');
final String result = await platform.invokeMethod('payOrder', {"zptoken": zpToken});
Step 3. Handling returned results
- At iOS side, set handler to Flutter Event channel. Then, send payment result to Flutter by implementing functions relevant to each result code:
//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])
}
- At Android side, set handler to Flutter Event channel. Then, send payment result to Flutter by implementing functions relevant to each result code:
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")
}
}
}
- At Flutter side, subscribe to the event channel created at native sides, then handle events relevant to the response code:
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()
Field | Data Type | Description |
---|---|---|
transactionId | string | The id of current transaction |
zpTranstoken | string | The token of current order |
appTransId | string | The app_trans_id of current transaction |
Case payment cancelled - Handle function paymentDidCanceled()
Field | Data Type | Description |
---|---|---|
zpTranstoken | string | The token of current order |
appTransId | string | The app_trans_id of current transaction |
Case payment cancelled by user - Handle function paymentDidError()
Field | Data Type | Description |
---|---|---|
errorCode | string | Code of returned error, is one of the values: UNKNOWN, PAYMENT_APP_NOT_FOUND, INPUT_IS_INVALID, EMPTY_RESULT, FAIL |
zpTranstoken | string | The token of current order |
appTransId | string | The app_trans_id of current transaction |
See also
What's next
- Finish coding? Verify your integration with testing.