Nhảy tới nội dung

Tích hợp React Native

Yêu cầu
  • Yêu cầu iOS >= 13.4
  • Yêu cầu Android: minSdkVersion = 21, compileSdkVersion = 34

Cài đặt cho Android

Bước 1: Tải SDK và cấu hình project

Lưu ý

Ứng dụng viết bằng Flutter hoặc tích hợp Flutter module không copy thư mục repo

  • Trong android/build.gradle, thêm cấu hình như sau
buildscript {
// ...

repositories {
// ...
jcenter()
flatDir {
dirs 'libs'
}
maven {
url 'https://jitpack.io'
}
}
//...
}

String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"

allprojects {
repositories {
google()
mavenCentral()
jcenter()
maven {
url '../app/libs/repo'
}
maven {
url "$storageUrl/download.flutter.io"
}
maven { url "https://jitpack.io" }
}
}

// ...
  • Trong android/gradle.properties, enable newArchEnabled cho project
newArchEnabled=true
  • Trong android/app/proguard-rules.pro, thêm cấu hình
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontwarn android.support.**
-dontwarn com.squareup.**
-dontwarn com.google.android.**
-verbose

# -keepattributes *Annotation*
-dontwarn lombok.**
-dontwarn io.realm.**

-dontwarn java.awt.**

# okhttp3
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn org.conscrypt.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

# Application classes that will be serialized/deserialized over Gson
-keep class vnpt.it3.econtract.data.model.** { *; }
-keep class vnpt.it3.econtract.data.** { *; }

# Java 8
-dontwarn java.lang.invoke.*
-dontwarn **$$Lambda$*

# Retrofit 2
-dontnote retrofit2.Platform
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-dontwarn retrofit2.Platform$Java8
-keepattributes Signature
-keepattributes Exceptions
-keepclassmembernames,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement


# pdf lib
-keep class com.shockwave.**

#To remove debug logs:
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** w(...);
public static *** i(...);
}

-keep public enum vnpt.it3.econtract.data.**{
*;
}

-keepclassmembers class **.R$* {
public static <fields>;
}
# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

-keep enum org.greenrobot.eventbus.ThreadMode { *; }
-keep class com.vnptit.innovation.sample.model.** { *; }
-keep class ai.icenter.face3d.native_lib.Face3DConfig { *; }
-keep class ai.icenter.face3d.native_lib.CardConfig { *; }
  • Trong android/app/build.gradle, thêm các cấu hình sau
android {
// ...
defaultConfig {
// ...
multiDexEnabled true
ndk {
// abiFilters 'armeabi-v7a', 'arm64-v8a','x86_64'
debugSymbolLevel 'FULL'
}
}
// ...
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
}

aaptOptions {
noCompress "bic"
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.aar"])
// ...

implementation 'com.vnpt.smartca.module.vnpt_smartca_module:flutter_release:1.0'

implementation files('libs/vnpt_smartca_sdk_lib-release-v1.0.aar')
implementation files('libs/ekyc_sdk-release-v3.2.7.aar')
implementation files('libs/eContract-v3.6.12.aar')
implementation 'com.auth0.android:jwtdecode:2.0.0'

implementation 'com.github.gcacace:signature-pad:1.3.1'
implementation "androidx.multidex:multidex:2.0.1"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

implementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
implementation 'com.github.mhiew:android-pdf-viewer:3.2.0-beta.3'

implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'com.karumi:dexter:6.0.0'

implementation 'com.android.support:exifinterface:28.0.0'
implementation 'com.google.code.gson:gson:2.10'

implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
}
  • Trong android/app/src/main/AndroidManifest.xml thêm các cấu hình sau
// Thêm quyền
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />

// Thêm vào thẻ <application>
tools:replace="android:allowBackup"
tools:targetApi="m"
android:allowBackup="false"
android:fullBackupContent="false"

// Thêm setup 2 activity
<activity android:name="io.flutter.embedding.android.FlutterFragmentActivity"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true"/>

<activity android:name="io.flutter.embedding.android.FlutterActivity"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true"/>

Bước 2: Tạo Native Modules (https://reactnative.dev/docs/native-modules-android)

  • Tạo file SmartCAModule.kt ngang hàng với MainApplication.kt như sau
package com.reactnativeapp
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.Toast
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.vnpt.smartca.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule

class SmartCAModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

override fun getName() = "SmartCAModule"

private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}

@ReactMethod
fun addListener(type: String?) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod
fun removeListeners(type: Int?) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod(isBlockingSynchronousMethod = true)
private fun createAccount() {
currentActivity?.runOnUiThread {
try {
VNPTSmartCA.createAccount { result ->
when (result.status) {
SmartCAResultCode.SUCCESS_CODE -> {
val obj: CallbackResult = Json.decodeFromString(
CallbackResult.serializer(), result.data.toString()
)
// SDK trả lại token, credential của khách hàng
// Đối tác tạo transaction cho khách hàng để lấy transId, sau đó gọi getWaitingTransaction
val token = obj.accessToken
val credentialId = obj.credentialId

val params = Arguments.createMap().apply {
putInt("code", 0)
putString("token", token)
putString("credentialId", credentialId)
}

sendEvent(this.reactApplicationContext, "EventReminder", params)
}
else -> {
val params = Arguments.createMap().apply {
putInt("code", 1)
putString("token", result.status.toString())
putString("credentialId", result.statusDesc)
}
//
sendEvent(this.reactApplicationContext, "EventReminder", params)
}
}
}
} catch (ex: Exception) {
throw ex;
}
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
private fun getAuth() {
currentActivity?.runOnUiThread {
try {
VNPTSmartCA.getAuthentication { result ->
when (result.status) {
SmartCAResultCode.SUCCESS_CODE -> {
val obj: CallbackResult = Json.decodeFromString(
CallbackResult.serializer(), result.data.toString()
)
// SDK trả lại token, credential của khách hàng
// Đối tác tạo transaction cho khách hàng để lấy transId, sau đó gọi getWaitingTransaction
val token = obj.accessToken
val credentialId = obj.credentialId
val serial = obj.serial

val params = Arguments.createMap().apply {
putInt("code", 0)
putString("token", token)
putString("credentialId", credentialId)
putString("serial", serial)
}

sendEvent(this.reactApplicationContext, "EventReminder", params)
}

else -> {
// Xử lý lỗi
val params = Arguments.createMap().apply {
putInt("code", 1)
putString("token", result.status.toString())
putString("credentialId", result.statusDesc)
}
//
sendEvent(this.reactApplicationContext, "EventReminder", params)
}
}
}
} catch (ex: Exception) {
throw ex;
}
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
private fun getMainInfo() {
currentActivity?.runOnUiThread {
try {
VNPTSmartCA.getMainInfo { result ->
when (result.status) {
SmartCAResultCode.SUCCESS_CODE -> {
// Xử lý khi confirm thành công
}

else -> {
// Xử lý khi confirm thất bại
}
}
}
} catch (ex: java.lang.Exception) {
throw ex;
}
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
private fun getWaitingTransaction(transId: String) {
currentActivity?.runOnUiThread {
try {
if (transId.isNullOrEmpty()) {
return;
}

VNPTSmartCA.getWaitingTransaction(transId) { result ->
val params = Arguments.createMap().apply {
putInt("code", 0)
}

sendEvent(this.reactApplicationContext, "EventReminder", params)

when (result.status) {
SmartCAResultCode.SUCCESS_CODE -> {
// Xử lý khi thành công
}
else -> {
// Xử lý khi thất bại
}
}
}
} catch (ex: Exception) {
throw ex;
}
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
private fun signOut() {
currentActivity?.runOnUiThread {
try {
VNPTSmartCA.signOut { result ->
when (result.status) {
SmartCAResultCode.SUCCESS_CODE -> {
val params = Arguments.createMap().apply {
putInt("code", 0)
}

sendEvent(this.reactApplicationContext, "EventReminder", params)
}

else -> {
val params = Arguments.createMap().apply {
putInt("code", 1)
}
sendEvent(this.reactApplicationContext, "EventReminder", params)
}
}
}
} catch (ex: Exception) {
throw ex;
}
}
}

companion object {

private var VNPTSmartCA = VNPTSmartCASDK()

@SuppressLint("StaticFieldLeak")
private lateinit var context: Context

fun init(context: Context) {
this.context = context

var customParams = CustomParams(
customerId = "", // Số CCCD, giấy tờ của KH đăng nhập SDK, để trống KH tự nhập
borderRadiusBtn = 999.0, // Border radius của button
colorSecondBtn = "", // Màu background nút phụ ví dụ: #FFFFFF
colorPrimaryBtn = "", // Màu background nút chính ví dụ: #4788FF
featuresLink = "", // Đường dẫn tới trang hướng dẫn sử dụng các tính năng sdk
customerPhone = "", // Số ĐT của KH
packageDefault = "", // Gói mặc định hiển thị khi mua Chứng thư số, ví dụ: PS0
password = "", // Mật khẩu mặc định khi tạo tài khoản
logoCustom = "", // Logo của đối tác theo mã hoá Base64 dạng "iVBORw0KGgoAAAANSUhEUgAAANgAAA......"
backgroundLogin = "" // Background của đối tác theo mã hoá Base64 dạng "iVBORw0KGgoAAAANSUhEUgAAANgAAA......"
)

var config = ConfigSDK(
clientId = "", // clientId tương ứng với môi trường được cấp qua email
clientSecret = "", // clientSecret tương ứng với môi trường được cấp qua email
env = SmartCAEnvironment.DEMO_ENV, // Môi trường kết nối DEMO/PROD
customParams = customParams,
lang = SmartCALanguage.VI, // Ngôn ngữ vi/en
isFlutter = true, // true nếu ứng dụng phát triển bằng Flutter, hoặc tích hợp Flutter module
)

VNPTSmartCA.initSDK(context, config)

val x = mutableListOf<Int>()
x.add(Intent.FLAG_ACTIVITY_NEW_TASK)
VNPTSmartCA.initCustomIntentFlag(x)
}

fun onDestroy() {
// super.onDestroy()
VNPTSmartCA.destroySDK();
}
}

}

@Serializable
data class CallbackResult(
val credentialId: String,
val accessToken: String,
val serial: String,
) : java.io.Serializable
  • Tạo file MyAppPackage.kt ngang hàng với MainApplication.kt
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager

class MyAppPackage : ReactPackage {
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()

override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(SmartCAModule(reactContext)).toMutableList()
}
  • Kết nối MyAppPackage vào function getPackages trong MainApplication
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
add(MyAppPackage())
}
  • Trong MainActivity thêm hai function sau
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SmartCAModule.init(this)
}

override fun onDestroy() {
super.onDestroy()
SmartCAModule.onDestroy()
}

Bước 3: Chi tiết các hàm chính

Cài đặt cho iOS

Bước 1: Tải SDK và cấu hình Project

  • Tải phiên bản SDK mới nhất từ link: https://github.com/VNPT-SmartCA/ios_vnptsmartca_sdk/releases

  • Kéo thả toàn bộ file .xcframework và .framework vào trong project. Đi tới Targets Project -> General -> Frameworks, Libraries, and Embedded Content

    Lưu ý

    Ứng dụng viết bằng Flutter hoặc tích hợp Flutter module chỉ kéo thả file ở thư mục eContract-eKYC

  • Bổ sung quyền truy cập Camera, FaceID trong Info.plist

    <key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) requires access to your phone’s camera</string>

<key>NSFaceIDUsageDescription</key>
<string>$(PRODUCT_NAME) Authentication with TouchId or FaceID</string>

Bước 2: Tạo Native Modules (https://reactnative.dev/docs/native-modules-ios)

  • Sử dụng xcode để tạo và mở project ios (*.xcworkspace)
  • Tạo file swift với tên là SmartCAModule.swift, lúc này xcode sẽ gợi ý tạo file để configure an Objective-C Bridging Header, chọn Create Bridging Header

import Foundation
import SmartCASDK
import FlutterPluginRegistrant

@objc(SmartCAModule)
class SmartCAModule: RCTEventEmitter {

private var manager: SmartCAManager?
private var hasListeners = false

override init() {
super.init()
self.manager = SmartCAManager.shared
}

override func startObserving() {
hasListeners = true
}

override func stopObserving() {
hasListeners = false
}

@objc func createAccount() {
DispatchQueue.main.async {
self.manager?.vnptSmartCASDK?.createAccount(callback: { result in
print(result)
})
}
}

@objc func signOut() {
DispatchQueue.main.async {
self.manager?.vnptSmartCASDK?.signOut(callback: { result in
if result.status == SmartCAResultCode.SUCCESS_CODE {
// Xử lý khi thành công
if self.hasListeners {
self.sendEvent(withName: "EventReminder", body: ["code": 0, "token": "Thông báo", "credentialId": "Đăng xuất thành công"])
}
} else {
// Xử lý khi thất bại
}
});
}
}

@objc func getAuth() {
DispatchQueue.main.async {
// SDK tự động xử lý các trường hợp về token: Hết hạn, chưa kích hoạt...
self.manager?.vnptSmartCASDK?.getAuthentication(callback: { result in
if result.status == SmartCAResultCode.SUCCESS_CODE {
// Xử lý khi thành công

if self.hasListeners {
self.sendEvent(withName: "EventReminder", body: ["code": 0, "token": result.data, "credentialId": ""])
}
} else {
// Xử lý khi thất bại
}
});
}
}

@objc func getMainInfo(){
DispatchQueue.main.async {
self.manager?.vnptSmartCASDK?.getMainInfo(callback: { result in
})
}
}

@objc func getWaitingTransaction(_ tranId: String) {
DispatchQueue.main.async {
self.manager?.vnptSmartCASDK?.getWaitingTransaction(tranId: tranId, callback: { result in
if result.status == SmartCAResultCode.SUCCESS_CODE {
print("Giao dịch thành công: \(result.status) - \(result.statusDesc) - \(result.data)");

if self.hasListeners {
self.sendEvent(withName: "EventReminder", body: ["code": 0, "token": result.status, "credentialId": result.statusDesc])
}


} else {
if self.hasListeners {
self.sendEvent(withName: "EventReminder", body: ["code": 1, "token": result.status, "credentialId": result.statusDesc])
}
}
});
}
}

override static func requiresMainQueueSetup() -> Bool {
return true
}
}

class SmartCAManager: NSObject {
static let shared = SmartCAManager()

@objc class func getInstance() -> SmartCAManager {
return SmartCAManager.shared
}

var vnptSmartCASDK: VNPTSmartCASDK?

@objc
func initSDK() {
if let ab = RCTPresentedViewController() {

let customParams = CustomParams(
customerId: "", // Số CCCD, giấy tờ của KH đăng nhập SDK, để trống KH tự nhập
borderRadiusBtn: 0, // Border radius của button
colorSecondBtn: "", // Màu background nút phụ vd: #FFFFFF
colorPrimaryBtn: "", // Màu background nút chính vd: #4788FF
featuresLink: "", // Đường dẫn tới trang hướng dẫn sử dụng các tính năng sdk
customerPhone: "", // Số ĐT mặc định của KH
packageDefault: "", // Gói mặc định hiển thị khi mua Chứng thư số
password: "", // Mật khẩu mặc định khi tạo tài khoản
logoCustom: "", // Logo của đối tác theo mã hoá Base64
backgroundLogin: "" // Background của đối tác theo mã hoá Base64
)

let config = SDKConfig(
clientId: "", // clientId tương ứng với môi trường được cấp qua email
clientSecret: "", // clientSecret tương ứng với môi trường được cấp qua email
environment: ENVIRONMENT.DEMO, // Môi trường kết nối DEMO/PROD
lang: LANG.VI, // Ngôn ngữ vi/en
isFlutterApp: false, // true nếu ứng dụng phát triển bằng Flutter, hoặc tích hợp Flutter module
customParams: customParams
)

self.vnptSmartCASDK = VNPTSmartCASDK(viewController: ab, config: config)

GeneratedPluginRegistrant.register(with: self.vnptSmartCASDK?.flutterEngine as! FlutterPluginRegistry);
}
}

@objc
func destroySDK() {
self.vnptSmartCASDK?.destroySDK();
}
}
  • Cấu hình file *-Bridging-Header.h
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
#import <React/RCTUtils.h>
  • Tạo file SmartCAModule.m
// RCTSmartCAModule.m
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

@interface RCT_EXTERN_MODULE(SmartCAModule, RCTEventEmitter)
RCT_EXTERN_METHOD(createAccount)
RCT_EXTERN_METHOD(getAuth)
RCT_EXTERN_METHOD(getMainInfo)
RCT_EXTERN_METHOD(getWaitingTransaction: String: String)
RCT_EXTERN_METHOD(signOut)
@end

Sử dụng trong React Native

  • Import native module
import {NativeModules} from 'react-native';
  • Khởi tạo
const {SmartCAModule} = NativeModules;
  • Viết hàm lắng nghe sự kiện EventReminder (có thể tạo nhiều sự kiện)
useEffect(() => {
const eventEmitter = new NativeEventEmitter(SmartCAModule)
let eventListener = eventEmitter.addListener('EventReminder', event => {
let code = event.code
let statusCode = event.statusCode
let statusDesc = event.statusDesc
let data = event.data

let testIOSEvent = eventEmitter.addListener('onIncrement', event => {
console.log('onIncrement event', event);
});

// Removes the listener once unmounted

return () => {
eventListener.remove();
testIOSEvent.remove();
};
}, []);
  • Sử dụng các hàm chính
// create account
SmartCAModule.createAccount()

// get auth
SmartCAModule.getAuth()

// get main info
SmartCAModule.getMainInfo()

// get waiting transaction
// Lấy accessToken từ `SmartCAModule.getAuth()`

let transactionId = 'xxx'
SmartCAModule.getWaitingTransaction(transactionId)

// sign out
SmartCAModule.signOut()

Bước 3: Chi tiết các hàm chính