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 with SPM
  spm_dependency(s,
    url: "https://github.com/appspanel/SPMAppsPanelSDK",
    requirement: { kind: "upToNextMajorVersion", minimumVersion: "5.6.0" },
    products: ["AppsPanelSDK"]
  )
  • 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

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"
}

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);
      })

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();