Apps Panel SDK Installation Guide for Expo 2
To include the Apps Panel SDK in an existing project, you need to follow this steps:
Create a local expo module using this command
npx create-expo-module@sdk-54 --local
Use your version of expo in the sdk-{expo_version} argument to avoid compatibility issues
You can use the following values when asked:
- Local Module Name: apsdk
- Native Module Name: APSDK
- Android Package Name: your android main package name.
This command will generate a module folder for iOS and Android
See Expo documentation if needed => Get Started
IOS
- Change the generated Podspec file in {yourModule/ios} to include the Apps Panel SDK with SPM
spm_dependency(s,
url: "https://github.com/appspanel/SPMAppsPanelSDK",
requirement: { kind: "upToNextMajorVersion", minimumVersion: "5.7.0" },
products: ["AppsPanelSDK"]
)
- Create an
APSDKSubscriber.swiftclass to modify lifecycle events and initialize the SDK as well as the PushNotification Manager.
import ExpoModulesCore
import AppsPanelSDK
public class APSDKSubscriber: ExpoAppDelegateSubscriber {
required public init() {}
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
try? AppsPanel.shared.configure(
withAppName: "appName", #Replace with your env value
appKey: "appKey", #Replace with your env value
privateKey: "privateKey" #Replace with your env value
)
try? AppsPanel.shared.startSession()
PushNotificationManager.shared.registerForPushNotifications(application: application)
PushNotificationManager.shared.checkReceivedNotification(
launchOptions: launchOptions,
state: application.applicationState
)
return true
}
public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
PushNotificationManager.shared.registerDevice(token: deviceToken)
}
}
- You also need to reference your Subscriber Class in the "expo-module.config.json" file of your Native Module like so:
"apple": {
"modules": [
"APSDKModule"
],
"appDelegateSubscribers": [
"APSDKSubscriber"
]
},
- You need to include your
GoogleService-Info.plistfile at the root of your project and add it inside your ios object in app.json
"googleServicesFile": "./GoogleService-Info.plist"- Running a prebuild command with the --clean arg is needed for your ios project to include those changes
npx expo prebuild --platform ios --clean- You should be able to run your ios project and see the SDK API calls in the Apps Panel Back Office.
More information => iOS AppDelegate Subscribers Documentation
Android
- To include the AppsPanel SDK in the android projet, we need to add the maven repository in the "expo-build-properties" of the "app.json" file under "plugins" sections
If you don't have the expo-build-properties package in your project, you can add it by doing the following
npx expo install expo-build-properties [
"expo-build-properties",
{
"android": {
"extraMavenRepos": [
{
"url": "https://repository.appspanel.com/repository/apnl-public-android-sdk/",
"authentication": "basic",
"credentials": {
"username": "guest",
"password": "CNONt0CkRzgqLpaEN92p"
}
}
],
"enableJetifier": true
}
}
]
- Create a
APSDKApplicationLifecycleListener.ktfile in the android folder to modify lifecycle events and initialize the SDK
( You can also create a Activity LifecyleListener if needed)
package com.yourpackage #Change for your package name
import com.appspanel.APSDK
import com.appspanel.manager.conf.APLocalConfiguration
import android.app.Application
import expo.modules.core.interfaces.ApplicationLifecycleListener
import com.appspanel.APSDKInterface
class APSDKApplicationLifecycleListener : ApplicationLifecycleListener, APSDKInterface {
override fun onCreate(application: Application) {
// Your setup code in `Application.onCreate`.
APSDK.install(
application,
APLocalConfiguration(
"appName", #Replace with your env value
"appKey", #Replace with your env value
"privateKey" #Replace with your env value
)
)
}
}
- Create a
APSDKPackage.ktfile to include the file above:
package com.yourpackage #Change for your package name
import android.content.Context
import expo.modules.core.interfaces.ApplicationLifecycleListener
import expo.modules.core.interfaces.Package
class APSDKPackage : Package {
override fun createApplicationLifecycleListeners(context: Context): List<ApplicationLifecycleListener> {
return listOf(APSDKApplicationLifecycleListener())
}
}
- Be careful at the generated APSDKModule.kt file package name, it should be your package name
package com.yourpackage
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
class APSDKModule : Module() {- You need to include your
google-services.jsonfile at the root of your project and register the file in your app.json inside the android object
"googleServicesFile": "./google-services.json",- You also need to include the Apps Panel SDK dependencies in your
module/android/build.gradlelike so:
android {
namespace "com.yourpackage"
defaultConfig {
versionCode 1
versionName "0.7.10"
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation 'com.appspanel.android:sdk:5.7.1'
implementation 'com.alibaba:fastjson:1.2.83'
}- Last step is to run the prebuild command on android to generate your android project and include those changes:
npx expo prebuild --platform android --cleanMore information => Android Lifecycle Listeners Documentation
You should be able to run your android project and see the SDK API calls in the Apps Panel Back Office.
You should now be able to use the Apps Panel SDK in your expo Project 🎉
Extra Configuration
Firebase dependency
Since AppsPanel SDK requires Firebase to be functional, we need to add the Expo dependency by running the following commands
npx expo install @react-native-firebase/app && npx expo install @react-native-firebase/crashlytics
Or simply adding those dependencies in the package.json
"@react-native-firebase/app": "^21.13.0",
"@react-native-firebase/crashlytics": "^21.13.0",
Next step is to register those plugins in the app.json file like so:
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/crashlytics"
]
And setting the "useFrameworks" property in the "ios" object to "static" as required by Firebase Official Docs
"ios": {
"deploymentTarget": "15.1",
"useFrameworks": "static"
}
Request Manager Implentation (API Calls)
Since you previously created a expo module, we can implement the request manager class to make API calls.
iOS
In the Module.swift file you can create the following method "doRequest" to manage API calls:
import AppsPanelSDK
import ExpoModulesCore
public class APSDKModule: Module {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
public func definition() -> ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('APSDK')` in JavaScript.
Name("APSDKModule")
// Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
Constants([
"texts": TextManager.shared.texts
])
AsyncFunction("doRequest") {
(
methodName: String,
endpoint: String,
parameters: [String: Any]?,
body: [String: Any]?,
headers: [String: String]?,
secureData: [String: Any]?
) async throws -> Any in
// Determine HTTP method
guard let method = AppsPanelSDK.HTTPMethod(rawValue: methodName) else {
throw NSError(
domain: "doRequest", code: 1000,
userInfo: [
NSLocalizedDescriptionKey: "Invalid HTTP method: \(methodName)"
])
}
// Set custom headers if needed
if let headers = headers {
AppsPanel.shared.customHeaders = headers
}
return try await withCheckedThrowingContinuation { continuation in
var requestBuilder = RequestManager.default
.request(endpoint, method: method, parameters: parameters ?? [:])
.setBody(body ?? [:])
.secureData(secureData ?? [:])
// If user token exists, add it
if AuthenticationTokenManager.token() != nil {
requestBuilder = requestBuilder.useUserToken()
}
requestBuilder.responseData { result in
switch result {
case .success(let response):
do {
let json = try JSONSerialization.jsonObject(with: response.data, options: [])
// Return as array or dictionary if possible
if let jsonArray = json as? [[String: Any]] {
continuation.resume(returning: jsonArray)
} else if let jsonDict = json as? [String: Any] {
continuation.resume(returning: jsonDict)
} else if let stringArray = json as? [String] {
continuation.resume(returning: stringArray)
} else {
let error = NSError(
domain: "", code: 1001,
userInfo: [
NSLocalizedDescriptionKey: "Response format is not parsable"
])
continuation.resume(throwing: error)
}
} catch {
continuation.resume(
throwing: NSError(
domain: "", code: 1002,
userInfo: [
NSLocalizedDescriptionKey: "Response format is not parsable",
NSUnderlyingErrorKey: error,
]))
}
case .failure(let error):
if let data = error.data, !data.isEmpty {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
continuation.resume(returning: jsonObject)
} catch {
continuation.resume(returning: "Error is not JSON")
}
} else {
continuation.resume(returning: "Error is not JSON")
}
}
}
}
}
}
}
Android
In the Module.kt you can create the following method "doRequest" to manage API calls
package APSDK
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import com.appspanel.APManager
import com.appspanel.manager.ws.APWSManager
import com.appspanel.manager.ws.APWSRequest
import com.appspanel.manager.ws.OnAPWSCallListener
import org.json.JSONObject
import org.json.JSONArray
import org.json.JSONException
import android.util.Log
import expo.modules.kotlin.Promise
class APSDKModule : Module() {
override fun definition() = ModuleDefinition {
Name("APSDKModule")
AsyncFunction("doRequest") { methodName: String, endpoint: String, parameters: Map<String, Any>?, body: Map<String, Any>?, headers: Map<String, String>?, secureData: Map<String, Any>?, promise: Promise ->
doRequest(methodName, endpoint, parameters, body, headers, secureData, promise)
}
}
private fun doRequest(
methodName: String,
endpoint: String,
parameters: Map<String, Any>?,
body: Map<String, Any>?,
headers: Map<String, String>?,
secureData: Map<String, Any>?,
promise: Promise
) {
var httpMethod = APWSManager.HttpMethod.GET
val token = APManager.instance.localConfiguration.userToken
when (methodName) {
"GET" -> httpMethod = APWSManager.HttpMethod.GET
"POST" -> httpMethod = APWSManager.HttpMethod.POST
"PATCH" -> httpMethod = APWSManager.HttpMethod.PATCH
"DELETE" -> httpMethod = APWSManager.HttpMethod.DELETE
"PUT" -> httpMethod = APWSManager.HttpMethod.PUT
}
val req = APWSRequest(httpMethod, endpoint, object : OnAPWSCallListener() {
override fun onNoConnection(request: APWSRequest, cacheContent: String, cacheTimestamp: Long) {
promise.reject("502", "No Connection", null)
}
override fun onResponse(request: APWSRequest, response: Any, headers: MutableMap<String, String>) {
if (response is String) {
val jsonString = response
try {
val jsonObject = JSONObject(jsonString)
promise.resolve(convertJsonToMap(jsonObject))
} catch (e1: JSONException) {
try {
val jsonArray = JSONArray(jsonString)
promise.resolve(mapOf("data" to convertJsonToArray(jsonArray)))
} catch (e2: JSONException) {
promise.reject("Error", "Invalid JSON format", e2)
}
}
} else {
promise.reject("Error", "Invalid response format", Exception("Response is not a valid JSON."))
}
}
override fun onError(request: APWSRequest, httpCode: Int, responseContent: String) {
val errorDetails = if (responseContent.isNotEmpty()) {
try {
val json = JSONObject(responseContent)
if (json.has("error")) {
mapOf(
"httpCode" to httpCode,
// directly return the inner "error" object
"error" to convertJsonToMap(json.getJSONObject("error"))
)
} else {
mapOf(
"httpCode" to httpCode,
"error" to convertJsonToMap(json)
)
}
} catch (e: JSONException) {
mapOf(
"httpCode" to httpCode,
"error" to responseContent
)
}
} else {
mapOf(
"httpCode" to httpCode,
"error" to "Empty response"
)
}
promise.resolve(errorDetails)
}
})
if (token != null) {
req.sendUserToken = true
}
try {
parameters?.let { params ->
for ((key, value) in params.entries) {
when (value) {
is String -> req.addGetParam(key, value)
is Int -> req.addGetParam(key, value)
is Boolean -> req.addGetParam(key, value)
is Long -> req.addGetParam(key, value)
is Float -> req.addGetParam(key, value)
is Double -> req.addGetParam(key, value.toInt())
else -> req.addGetParam(key, value.toString())
}
}
}
headers?.let { headerMap ->
for ((key, value) in headerMap.entries) {
req.addHeader(key, value)
}
}
} catch (e: Exception) {
Log.d("Exception PARAM", e.message ?: "Unknown error")
}
try {
body?.let { bodyMap ->
val bodyString = JSONObject(bodyMap).toString()
req.addJsonParam(bodyString)
}
} catch (e: JSONException) {
Log.d("Exception BODY", e.message ?: "Unknown error")
}
APWSManager.doRequest(req)
}
private fun convertJsonToMap(jsonObject: JSONObject): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = jsonObject.get(key)
map[key] = when (value) {
is JSONObject -> convertJsonToMap(value)
is JSONArray -> convertJsonToArray(value)
JSONObject.NULL -> null
else -> value
}
}
return map
}
private fun convertJsonToArray(jsonArray: JSONArray): List<Any?> {
val list = mutableListOf<Any?>()
for (i in 0 until jsonArray.length()) {
val value = jsonArray.get(i)
list.add(when (value) {
is JSONObject -> convertJsonToMap(value)
is JSONArray -> convertJsonToArray(value)
JSONObject.NULL -> null
else -> value
})
}
return list
}
}
React Native (Expo)
In the Module.ts file you need to implement the DoRequest method like so:
import { NativeModule, requireNativeModule } from 'expo';
import { APSDKModuleEvents } from './APSDK.types';
type RequestParams = Record<string, any> | null;
type Headers = Record<string, string> | null;
declare class APSDKModule extends NativeModule<APSDKModuleEvents> {
doRequest(
methodName: string,
endpoint: string,
parameters?: RequestParams,
body?: RequestParams,
headers?: Headers,
secureData?: RequestParams
): Promise<void>;
}
// This call loads the native module object from the JSI.
export default requireNativeModule<APSDKModule>('APSDKModule');
In your App.tsx or other expo file, you can call this method by doing:
GET Method
APSDK.doRequest('GET', 'news', { limit: 2, offset: 0 }, null, {'x-ap-test': "1234"})
.then((response) => {
console.log('doRequest response:', response);
}).catch((error) => {
console.error('Error calling doRequest:', error);
})
POST Method
APSDK.doRequest('POST','endpoint', null, { key: 'value' }, {'x-ap-test': "1234"})
.then((response) => {
console.log('doRequest response:', response);
}).catch((error) => {
console.error('Error calling doRequest:', error);
})
Push Notifications
Since the automatic request of the permission can be disabled on the Back Office, you can request the permission by implementing the requestPermissionIfNeeded method in your module.
Android
import com.appspanel.manager.push.APPushManager
override fun definition() = ModuleDefinition {
Name("APSDKModule")
AsyncFunction("doRequest") { methodName: String, endpoint: String, parameters: Map<String, Any>?, body: Map<String, Any>?, headers: Map<String, String>?, secureData: Map<String, Any>?, promise: Promise ->
doRequest(methodName, endpoint, parameters, body, headers, secureData, promise)
}
Function("requestPushNotificationPermission"){
APPushManager.requestPermissionIfNeeded()
}
}
iOS
Function("requestPushNotificationPermission") {
guard let application = UIApplication.shared as UIApplication? else {
return
}
PushNotificationManager.shared
.registerForPushNotifications(application: application)
}
Using this method allows you to request the permission at any moment in your application like so:
APSDK.requestPushNotificationPermission();Updated about 19 hours ago