Apps Panel SDK Installation Guide for Expo
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@latest --local
This command will generate a module folder for iOS and Android
See => Get Started
IOS
- Change the generated Podspec file in {yourModule/ios} to include the Apps Panel SDK
s.dependency 'AppsPanelSDKv5'
- Create an AppDelegate Subscriber class 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",
appKey: "appKey",
privateKey: "privateKey"
)
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"
]
},
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
[
"expo-build-properties",
{
"android": {
"extraMavenRepos": [
{
"url": "https://repository.appspanel.com/repository/apnl-internal-android-sdk/",
"credentials": {
"username": "guest-internal",
"password": "YECyaNuqPCSt85t9Pdhh"
}
}
],
"enableJetifier": true
},
"ios": {
"deploymentTarget": "15.1"
}
}
]
- Create a Application LifecycleListener file in the android folder to modify lifecycle events and initialize the SDK
( You can also create a Activity LifecyleListener if needed)
package APSDK
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",
"appKey",
"privateKey"
)
)
}
}
- Create a package file to include the file above:
package APSDK
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())
}
}
More information => Android Lifecycle Listeners Documentation
Extra Configuration
Since Expo's ios and android folder are generated and iOS Podfile modifications are needed to make the Apps Panel SDK work, we need to use config plugins to modify those files. Here's an example on how to add the "use_frameworks!" line to the Podfile as well as the post install script
// withModifiedPodfile.js
const { withDangerousMod } = require('@expo/config-plugins');
const { readFileSync, writeFileSync } = require('fs');
const path = require('path');
/**
* Config plugin to modify the generated Podfile by:
* 1. Adding "use_frameworks!" under the target declaration
* 2. Adding BUILD_LIBRARY_FOR_DISTRIBUTION setting in post_install script
*/
const withModifiedPodfile = (config) => {
return withDangerousMod(config, [
'ios',
async (config) => {
const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
// Read the generated Podfile
let podfileContent = readFileSync(podfilePath, 'utf8');
// Add "use_frameworks!" under the target declaration
// Look for the target pattern and add our line after it
const targetPattern = /target ['"].*['"] do/;
podfileContent = podfileContent.replace(
targetPattern,
(match) => `${match}\n use_frameworks!`
);
// Add BUILD_LIBRARY_FOR_DISTRIBUTION setting in post_install script
const buildLibraryForDistributionBlock = `
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
end
end`;
// Check if post_install hook already exists
if (podfileContent.includes('post_install do |installer|')) {
// Post_install exists, inject our code at the beginning of the block
podfileContent = podfileContent.replace(
/post_install do \|installer\|([\s\S]*?)end/m,
(match, postInstallContent) => {
if (match.includes("BUILD_LIBRARY_FOR_DISTRIBUTION")) {
// The setting is already there, don't duplicate it
return match;
}
return `post_install do |installer|${buildLibraryForDistributionBlock}${postInstallContent}end`;
}
);
} else {
// No post_install hook, add one at the end of the file
podfileContent = `${podfileContent.trim()}\n\npost_install do |installer|${buildLibraryForDistributionBlock}\nend\n`;
}
// Write the modified content back to the Podfile
writeFileSync(podfilePath, podfileContent);
return config;
},
]);
};
module.exports = withModifiedPodfile;
To include those modifications, we need to add this config plugin to the "plugins" section in the "app.json" file like so:
"plugins": [
"./plugins/withModifiedPodfile.js", ---> HERE
[
"expo-build-properties",
{
"android": {
"extraMavenRepos": [
{
"url": "https://repository.appspanel.com/repository/apnl-internal-android-sdk/",
"credentials": {
"username": "guest-internal",
"password": "YECyaNuqPCSt85t9Pdhh"
}
}
],
"enableJetifier": true
},
"ios": {
"deploymentTarget": "15.1"
}
}
]
]
Android Manifest Modifications
To avoid crashes when reinstalling the application using latest version of our SDK (5.6) we need to disable the property "allowBackup" in the AndroidManifest.xml The best way to implement this is to create a plugin like so:
const { withAndroidManifest } = require("@expo/config-plugins");
module.exports = function withAllowBackup(config) {
return withAndroidManifest(config, async (config) => {
const androidManifest = config.modResults;
if (
androidManifest.manifest &&
androidManifest.manifest.application &&
androidManifest.manifest.application[0].$
) {
// Set allowBackup="false"
androidManifest.manifest.application[0]["$"]["android:allowBackup"] = "false";
}
return config;
});
};
Don't forget to add the register the plugin in the app.json file:
"plugins": [
"./plugins/withModifiedPodfile.js",
"./plugins/withAllowBackup.js" ---> HERE
[
"expo-build-properties",
{
"android": {
"extraMavenRepos": [
{
"url": "https://repository.appspanel.com/repository/apnl-internal-android-sdk/",
"credentials": {
"username": "guest-internal",
"password": "YECyaNuqPCSt85t9Pdhh"
}
}
],
"enableJetifier": true
},
"ios": {
"deploymentTarget": "15.1"
}
}
]
]
The final step is to run the prebuild command to generate the ios and android folders with the modifications needed:
npx expo prebuild
You should now be able to use the Apps Panel SDK in your expo Project 🎉
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);
})
Updated 5 days ago