diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 10871aa..1d11128 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -16,10 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- cache: 'npm'
+ cache: 'yarn'
- run: yarn
- run: yarn lint
- run: yarn typecheck
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a598eb1
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,24 @@
+# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
+name: Test
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [lts/*]
+ # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases
+ steps:
+ - uses: actions/checkout@v3
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'yarn'
+ - run: yarn
+ - run: yarn test
diff --git a/.gitignore b/.gitignore
index ba774bf..c0eae07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,11 +129,11 @@ dist
.yarn/install-state.gz
.pnp.*
-
lib/
example/.bundle/
example/ios/build/
example/ios/Pods/
+example/ios/ExampleAppExample.xcworkspace/xcuserdata/*
**/*/.gradle/
example/android/app/build/
example/vendor/
@@ -142,4 +142,4 @@ example/android/local.properties
android/build
# Exclude because this repo forces to use yarn instead of npm
-package-lock.json
\ No newline at end of file
+package-lock.json
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 0000000..b0c1c68
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 202e1f7..c53c64c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,9 +23,9 @@ The [example app](/example/) demonstrates usage of the library. You need to run
It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app.
-If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/ExampleAppExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-example-app`.
+If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/ExampleAppExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > abrevva-react-native`.
-To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-example-app` under `Android`.
+To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `abrevva-react-native` under `Android`.
You can use various commands from the root directory to work with the project.
diff --git a/react-native-example-app.podspec b/abrevva-react-native.podspec
similarity index 95%
rename from react-native-example-app.podspec
rename to abrevva-react-native.podspec
index 3ebd4c1..36135ce 100644
--- a/react-native-example-app.podspec
+++ b/abrevva-react-native.podspec
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
- s.name = "react-native-example-app"
+ s.name = "abrevva-react-native"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
s.dependency "CocoaMQTT"
s.dependency "CryptoSwift"
- s.dependency "AbrevvaSDK", '1.0.15'
+ s.dependency "AbrevvaSDK", '~> 1.0.20'
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
diff --git a/android/build.gradle b/android/build.gradle
index f0df3a4..6c1f4a3 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,106 +1,105 @@
-buildscript {
- // Buildscript is evaluated before everything else so we can't use getExtOrDefault
- def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ExampleApp_kotlinVersion"]
+import com.android.Version
- repositories {
- google()
- mavenCentral()
- }
+buildscript {
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["AbrevvaReactNative_kotlinVersion"]
- dependencies {
- classpath "com.android.tools.build:gradle:7.2.1"
- // noinspection DifferentKotlinGradleVersion
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
+ repositories {
+ google()
+ mavenCentral()
+ }
-def reactNativeArchitectures() {
- def value = rootProject.getProperties().get("reactNativeArchitectures")
- return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.2.2"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
}
def isNewArchitectureEnabled() {
- return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
if (isNewArchitectureEnabled()) {
- apply plugin: "com.facebook.react"
+ apply plugin: "com.facebook.react"
}
def getExtOrDefault(name) {
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ExampleApp_" + name]
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["AbrevvaReactNative_" + name]
}
def getExtOrIntegerDefault(name) {
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ExampleApp_" + name]).toInteger()
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["AbrevvaReactNative_" + name]).toInteger()
}
-def supportsNamespace() {
- def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
- def major = parsed[0].toInteger()
- def minor = parsed[1].toInteger()
+static def supportsNamespace() {
+ def parsed = Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
+ def major = parsed[0].toInteger()
+ def minor = parsed[1].toInteger()
- // Namespace support was added in 7.3.0
- return (major == 7 && minor >= 3) || major >= 8
+ // Namespace support was added in 7.3.0
+ return (major == 7 && minor >= 3) || major >= 8
}
android {
- if (supportsNamespace()) {
- namespace "com.exampleapp"
-
- sourceSets {
- main {
- manifest.srcFile "src/main/AndroidManifestNew.xml"
- }
+ if (supportsNamespace()) {
+ namespace "com.evva.xesar.abrevva.reactnative"
+
+ sourceSets {
+ main {
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
+ }
+ }
}
- }
- compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
- defaultConfig {
- minSdkVersion getExtOrIntegerDefault("minSdkVersion")
- targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
+ defaultConfig {
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
- }
+ }
- buildTypes {
- release {
- minifyEnabled false
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
}
- }
- lintOptions {
- disable "GradleCompatible"
- }
+ lintOptions {
+ disable "GradleCompatible"
+ }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
repositories {
- google()
- mavenCentral()
- mavenLocal()
- maven {
- name = "evva-github"
- url = url = uri("https://maven.pkg.github.com/evva-sfw/abrevva-sdk-android")
- }
+ google()
+ mavenCentral()
+ mavenLocal()
}
def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
- // For < 0.71, this will be from the local maven repo
- // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
- //noinspection GradleDynamicVersion
- implementation "com.facebook.react:react-native:+"
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- implementation group: "com.evva.xesar", name: "abrevva-sdk-android", version: "1.0.19"
-
+ implementation group: "com.evva.xesar", name: "abrevva-sdk-android", version: "1.0.19"
+
+ implementation "com.facebook.react:react-native:0.20.1"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
+
+ testImplementation "androidx.test:core-ktx:1.6.1"
+ testImplementation "junit:junit:4.13.2"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
+ testImplementation "org.junit.jupiter:junit-jupiter:5.10.0"
+ testImplementation "org.junit.jupiter:junit-jupiter:5.10.0"
+ testImplementation "org.junit.platform:junit-platform-suite-engine:1.10.0"
+ testImplementation "io.mockk:mockk:1.13.12"
+
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
-
diff --git a/android/gradle.properties b/android/gradle.properties
index 03c7936..c51f777 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
-ExampleApp_kotlinVersion=1.7.0
-ExampleApp_minSdkVersion=29
-ExampleApp_targetSdkVersion=31
-ExampleApp_compileSdkVersion=31
-ExampleApp_ndkversion=21.4.7075529
+AbrevvaReactNative_kotlinVersion=1.9.22
+AbrevvaReactNative_minSdkVersion=29
+AbrevvaReactNative_targetSdkVersion=34
+AbrevvaReactNative_compileSdkVersion=34
+AbrevvaReactNative_ndkversion=21.4.7075529
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
index ccebba7..2c35211 100644
Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 42defcc..e6aba25 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/android/gradlew b/android/gradlew
index 79a61d4..f5feea6 100755
--- a/android/gradlew
+++ b/android/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +134,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/android/gradlew.bat b/android/gradlew.bat
index 6689b85..9b42019 100644
--- a/android/gradlew.bat
+++ b/android/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index ddfd609..be24cc6 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,27 +1,29 @@
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:allowBackup="false"
+ android:theme="@style/AppTheme">
+
+
+
+
+
+
diff --git a/android/src/main/java/com/exampleapp/AbrevvaBleModule.kt b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModule.kt
similarity index 86%
rename from android/src/main/java/com/exampleapp/AbrevvaBleModule.kt
rename to android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModule.kt
index 2f32428..f343798 100644
--- a/android/src/main/java/com/exampleapp/AbrevvaBleModule.kt
+++ b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModule.kt
@@ -1,37 +1,25 @@
-package com.exampleapp
+package com.evva.xesar.abrevva.reactnative
-import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
+import com.evva.xesar.abrevva.ble.BleManager
+import com.evva.xesar.abrevva.nfc.toHexString
+import com.evva.xesar.abrevva.util.bytesToString
+import com.evva.xesar.abrevva.util.stringToBytes
+import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
-import java.util.UUID
-import com.evva.xesar.abrevva.ble.BleManager
-import com.evva.xesar.abrevva.util.bytesToString
-import com.evva.xesar.abrevva.util.stringToBytes
-import com.evva.xesar.abrevva.nfc.toHexString
-
-import com.facebook.react.ReactActivity
-import com.facebook.react.bridge.Arguments
-import com.facebook.react.bridge.ReactContext.RCTDeviceEventEmitter
-import com.facebook.react.bridge.ReadableArray
-import com.facebook.react.modules.core.DeviceEventManagerModule
-import com.facebook.react.modules.core.PermissionListener
-import com.facebook.react.modules.core.RCTNativeAppEventEmitter
-import com.facebook.react.uimanager.events.RCTModernEventEmitter
-import io.reactivex.annotations.NonNull
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult
-import org.json.JSONArray
+import java.util.UUID
class AbrevvaBleModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
@@ -49,7 +37,8 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var neverForLocation = false
try {
neverForLocation = options.getBoolean("androidNeverForLocation")
- } catch (_: Exception){}
+ } catch (_: Exception) {
+ }
this.aliases = if (neverForLocation) {
arrayOf(
@@ -66,14 +55,18 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
} else {
this.aliases = arrayOf(
android.Manifest.permission.ACCESS_COARSE_LOCATION,
- android.Manifest.permission.ACCESS_FINE_LOCATION,
- android.Manifest.permission.BLUETOOTH,
- android.Manifest.permission.BLUETOOTH_ADMIN,
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ android.Manifest.permission.BLUETOOTH,
+ android.Manifest.permission.BLUETOOTH_ADMIN,
)
}
this.aliases.forEach {
- if (ContextCompat.checkSelfPermission(reactApplicationContext, it) == PackageManager.PERMISSION_DENIED){
+ if (ContextCompat.checkSelfPermission(
+ reactApplicationContext,
+ it
+ ) == PackageManager.PERMISSION_DENIED
+ ) {
ActivityCompat.requestPermissions(
currentActivity!!,
this.aliases,
@@ -105,7 +98,7 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
}
@ReactMethod
- fun isLocationEnabled( promise: Promise) {
+ fun isLocationEnabled(promise: Promise) {
val result = Arguments.createMap()
result.putBoolean("value", manager.isLocationEnabled())
@@ -165,7 +158,8 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var timeout: Long = 10000
try {
timeout = options.getDouble("timeout").toLong()
- } catch (_:Exception){}
+ } catch (_: Exception) {
+ }
this.manager.startScan({ success: Boolean ->
if (success) {
@@ -198,7 +192,8 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var timeout: Long = 10000
try {
timeout = options.getDouble("timeout").toLong()
- } catch (_:Exception){}
+ } catch (_: Exception) {
+ }
manager.connect(deviceId, { success: Boolean ->
if (success) {
@@ -230,20 +225,27 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var timeout: Long = 10000
try {
timeout = options.getDouble("timeout").toLong()
- } catch (_:Exception) {}
+ } catch (_: Exception) {
+ }
val characteristic = getCharacteristic(options, promise)
?: return promise.reject("read(): bad characteristic")
- manager.read(deviceId, characteristic.first, characteristic.second, { success: Boolean, data: ByteArray? ->
- if (success) {
- val ret = Arguments.createMap()
- ret.putString("value", bytesToString(data!!))
- promise.resolve(ret)
- } else {
- promise.reject("read(): failed to read from device")
- }
- }, timeout)
+ manager.read(
+ deviceId,
+ characteristic.first,
+ characteristic.second,
+ { success: Boolean, data: ByteArray? ->
+ if (success) {
+ val ret = Arguments.createMap()
+ ret.putString("value", bytesToString(data!!))
+ promise.resolve(ret)
+ } else {
+ promise.reject("read(): failed to read from device")
+ }
+ },
+ timeout
+ )
}
@ReactMethod
@@ -253,10 +255,12 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var timeout: Long = 10000
try {
timeout = options.getDouble("timeout").toLong()
- }catch (_:Exception){}
+ } catch (_: Exception) {
+ }
val characteristic =
- getCharacteristic(options, promise) ?: return promise.reject("read(): bad characteristic")
+ getCharacteristic(options, promise)
+ ?: return promise.reject("read(): bad characteristic")
val value =
options.getString("value") ?: return promise.reject("write(): missing value for write")
@@ -287,7 +291,8 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
var isPermanentRelease = false
try {
isPermanentRelease = options.getBoolean("isPermanentRelease")
- } catch (_:Exception){}
+ } catch (_: Exception) {
+ }
manager.disengage(
deviceId,
@@ -298,7 +303,7 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
isPermanentRelease
) { status: Any ->
val result = Arguments.createMap()
- result.putString("value", status as String) // TODO: Check if this works
+ result.putString("value", status as String)
promise.resolve(result)
}
@@ -316,20 +321,20 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
deviceId,
characteristic.first,
characteristic.second,
- { success:Boolean ->
+ { success: Boolean ->
if (success) {
promise.resolve("success")
} else {
promise.reject("startNotifications(): failed to set notifications")
}
}, { data: ByteArray ->
- val key =
- "notification|${deviceId}|${(characteristic.first)}|${(characteristic.second)}"
+ val key =
+ "notification|${deviceId}|${(characteristic.first)}|${(characteristic.second)}"
- val ret = Arguments.createMap()
- ret.putString("value", bytesToString(data))
- reactApplicationContext.emitDeviceEvent(key, ret)
- })
+ val ret = Arguments.createMap()
+ ret.putString("value", bytesToString(data))
+ reactApplicationContext.emitDeviceEvent(key, ret)
+ })
}
@ReactMethod
@@ -340,7 +345,11 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
getCharacteristic(options, promise)
?: return promise.reject("stopNotifications(): bad characteristic")
- manager.stopNotifications(deviceId, characteristic.first, characteristic.second) { success: Boolean ->
+ manager.stopNotifications(
+ deviceId,
+ characteristic.first,
+ characteristic.second
+ ) { success: Boolean ->
if (success) {
promise.resolve("success")
} else {
@@ -427,7 +436,9 @@ class AbrevvaBleModule(reactContext: ReactApplicationContext) :
// Extract EVVA manufacturer-id
var arr = byteArrayOf(0x01)
arr.toHexString()
- val keyHex = byteArrayOf(scanRecordBytes.getByte(6)!!).toHexString() + byteArrayOf(scanRecordBytes.getByte(5)!!).toHexString()
+ val keyHex = byteArrayOf(scanRecordBytes.getByte(6)!!).toHexString() + byteArrayOf(
+ scanRecordBytes.getByte(5)!!
+ ).toHexString()
val keyDec = keyHex.toInt(16)
// Slice out manufacturer data
diff --git a/android/src/main/java/com/exampleapp/AbrevvaCryptoModule.kt b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModule.kt
similarity index 74%
rename from android/src/main/java/com/exampleapp/AbrevvaCryptoModule.kt
rename to android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModule.kt
index bbc0ab2..49dde8f 100644
--- a/android/src/main/java/com/exampleapp/AbrevvaCryptoModule.kt
+++ b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModule.kt
@@ -1,8 +1,5 @@
-package com.exampleapp
+package com.evva.xesar.abrevva.reactnative
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.bridge.ReactContextBaseJavaModule
-import com.facebook.react.bridge.ReactMethod
import com.evva.xesar.abrevva.crypto.AesCCM
import com.evva.xesar.abrevva.crypto.AesGCM
import com.evva.xesar.abrevva.crypto.HKDF
@@ -10,12 +7,14 @@ import com.evva.xesar.abrevva.crypto.SimpleSecureRandom
import com.evva.xesar.abrevva.crypto.X25519Wrapper
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import org.bouncycastle.util.encoders.Base64
import org.bouncycastle.util.encoders.Hex
import java.io.BufferedInputStream
import java.io.FileOutputStream
-import java.io.IOException
import java.net.URL
import java.nio.file.Paths
@@ -24,10 +23,10 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
@ReactMethod
fun encrypt(options: ReadableMap, promise: Promise) {
- val key = Hex.decode(options.getString("key")?: "")
- val iv = Hex.decode(options.getString("iv")?: "")
- val adata = Hex.decode(options.getString("adata")?: "")
- val pt = Hex.decode(options.getString("pt")?: "")
+ val key = Hex.decode(options.getString("key") ?: "")
+ val iv = Hex.decode(options.getString("iv") ?: "")
+ val adata = Hex.decode(options.getString("adata") ?: "")
+ val pt = Hex.decode(options.getString("pt") ?: "")
val tagLength = options.getInt("tagLength")
val ct: ByteArray = AesCCM.encrypt(key, iv, adata, pt, tagLength)
@@ -38,7 +37,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
System.arraycopy(ct, pt.size, authTag, 0, tagLength)
if (ct.isEmpty()) {
- promise.reject("encrypt(): encryption failed")
+ promise.reject(Exception("encrypt(): encryption failed"))
} else {
val ret = Arguments.createMap()
ret.putString("cipherText", Hex.toHexString(cipherText))
@@ -49,16 +48,16 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
@ReactMethod
fun decrypt(options: ReadableMap, promise: Promise) {
- val key = Hex.decode(options.getString("key")?: "")
- val iv = Hex.decode(options.getString("iv")?: "")
- val adata = Hex.decode(options.getString("adata")?: "")
- val ct = Hex.decode(options.getString("ct")?: "")
+ val key = Hex.decode(options.getString("key") ?: "")
+ val iv = Hex.decode(options.getString("iv") ?: "")
+ val adata = Hex.decode(options.getString("adata") ?: "")
+ val ct = Hex.decode(options.getString("ct") ?: "")
val tagLength = options.getInt("tagLength")
val pt: ByteArray = AesCCM.decrypt(key, iv, adata, ct, tagLength)
- if (ct.isEmpty()) {
- promise.reject("decrypt(): decryption failed")
+ if (pt.isEmpty()) {
+ promise.reject(Exception("decrypt(): decryption failed"))
} else {
val ret = Arguments.createMap()
ret.putString("plainText", Hex.toHexString(pt))
@@ -77,7 +76,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
ret.putString("publicKey", Base64.toBase64String(keyPair.publicKey))
promise.resolve(ret)
} catch (e: Exception) {
- promise.reject("generateKeyPair(): private key creation failed")
+ promise.reject(Exception("generateKeyPair(): private key creation failed"))
}
}
@@ -103,7 +102,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
ret.putString("sharedSecret", Hex.toHexString(sharedSecret))
promise.resolve(ret)
} catch (e: Exception) {
- promise.reject("computeSharedSecret(): failed to create shared key")
+ promise.reject(Exception("computeSharedSecret(): failed to create shared key"))
}
}
@@ -112,17 +111,17 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
try {
val ptPath = options.getString("ptPath")
if (ptPath == null || ptPath == "") {
- promise.reject("encryptFile(): invalid ptPath")
+ promise.reject(Exception("encryptFile(): invalid ptPath"))
return
}
val ctPath = options.getString("ctPath")
if (ctPath == null || ctPath == "") {
- promise.reject("encryptFile(): invalid ctPath")
+ promise.reject(Exception("encryptFile(): invalid ctPath"))
return
}
val sharedSecret = options.getString("sharedSecret")
if (sharedSecret == null || sharedSecret == "") {
- promise.reject("encryptFile(): invalid shared secret")
+ promise.reject(Exception("encryptFile(): invalid shared secret"))
return
}
@@ -133,7 +132,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
ret.putBoolean("opOk", operationOk)
promise.resolve(ret)
} catch (e: Exception) {
- promise.reject("encryptFile(): failed to encrypt file")
+ promise.reject(e)
}
}
@@ -142,17 +141,17 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
try {
val sharedSecret = options.getString("sharedSecret")
if (sharedSecret == null || sharedSecret == "") {
- promise.reject("decryptFile(): invalid shared secret")
+ promise.reject(Exception("decryptFile(): invalid shared secret"))
return
}
val ctPath = options.getString("ctPath")
if (ctPath == null || ctPath == "") {
- promise.reject("decryptFile(): invalid ctPath")
+ promise.reject(Exception("decryptFile(): invalid ctPath"))
return
}
val ptPath = options.getString("ptPath")
if (ptPath == null || ptPath == "") {
- promise.reject("decryptFile(): invalid ptPath")
+ promise.reject(Exception("decryptFile(): invalid ptPath"))
return
}
@@ -163,7 +162,20 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
ret.putBoolean("opOk", operationOk)
promise.resolve(ret)
} catch (e: Exception) {
- promise.reject("decryptFile(): failed to decrypt file")
+ promise.reject(e)
+ }
+ }
+
+ fun writeToFile(ctPath: String, url: String) {
+
+ BufferedInputStream(URL(url).openStream()).use { `in` ->
+ FileOutputStream(ctPath).use { fileOutputStream ->
+ val dataBuffer = ByteArray(4096)
+ var bytesRead: Int
+ while (`in`.read(dataBuffer, 0, 4096).also { bytesRead = it } != -1) {
+ fileOutputStream.write(dataBuffer, 0, bytesRead)
+ }
+ }
}
}
@@ -171,33 +183,25 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
fun decryptFileFromURL(options: ReadableMap, promise: Promise) {
val sharedSecret = options.getString("sharedSecret")
if (sharedSecret == null || sharedSecret == "") {
- promise.reject("decryptFileFromURL(): invalid shared secret")
+ promise.reject(Exception("decryptFileFromURL(): invalid shared secret"))
return
}
val url = options.getString("url")
if (url == null || url == "") {
- promise.reject("decryptFileFromURL(): invalid url")
+ promise.reject(Exception("decryptFileFromURL(): invalid url"))
return
}
val ptPath = options.getString("ptPath")
if (ptPath == null || ptPath == "") {
- promise.reject("decryptFileFromURL(): invalid ptPath")
+ promise.reject(Exception("decryptFileFromURL(): invalid ptPath"))
return
}
val ctPath = Paths.get(ptPath).parent.toString() + "/blob"
try {
- BufferedInputStream(URL(url).openStream()).use { `in` ->
- FileOutputStream(ctPath).use { fileOutputStream ->
- val dataBuffer = ByteArray(4096)
- var bytesRead: Int
- while (`in`.read(dataBuffer, 0, 4096).also { bytesRead = it } != -1) {
- fileOutputStream.write(dataBuffer, 0, bytesRead)
- }
- }
- }
- } catch (e: IOException) {
- promise.reject("decryptFileFromURL(): failed to load data from url")
+ writeToFile(ptPath, url)
+ } catch (e: Exception) {
+ promise.reject(e)
return
}
@@ -209,7 +213,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
ret.putBoolean("opOk", operationOk)
promise.resolve(ret)
} catch (e: Exception) {
- promise.reject("decryptFileFromURL(): failed to decrypt from file")
+ promise.reject(e)
}
}
@@ -219,7 +223,7 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
val rnd: ByteArray = SimpleSecureRandom.getSecureRandomBytes(numBytes)
if (rnd.isEmpty()) {
- promise.reject("random(): random generation failed")
+ promise.reject(Exception("random(): random generation failed"))
} else {
val ret = Arguments.createMap()
ret.putString("value", Hex.toHexString(rnd))
@@ -232,11 +236,11 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
val key = Hex.decode(options.getString("key") ?: "")
val salt = Hex.decode(options.getString("salt") ?: "")
val info = Hex.decode(options.getString("info") ?: "")
- val length = options.getInt("length")
+ val length = options.getInt("length") ?: 0
val derived: ByteArray = HKDF.derive(key, salt, info, length)
if (derived.isEmpty()) {
- promise.reject("derive(): key derivation failed")
+ promise.reject(Exception("derive(): key derivation failed"))
} else {
val ret = Arguments.createMap()
ret.putString("value", Hex.toHexString(derived))
@@ -251,5 +255,5 @@ class AbrevvaCryptoModule(reactContext: ReactApplicationContext) :
companion object {
const val NAME = "AbrevvaCrypto"
}
-
+
}
\ No newline at end of file
diff --git a/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaNfcModule.kt b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaNfcModule.kt
new file mode 100644
index 0000000..26e9d08
--- /dev/null
+++ b/android/src/main/java/com/evva/xesar/abrevva/reactnative/AbrevvaNfcModule.kt
@@ -0,0 +1,163 @@
+package com.evva.xesar.abrevva.reactnative
+
+import android.content.Intent
+import android.nfc.NfcAdapter
+import com.evva.xesar.abrevva.nfc.KeyStoreHandler
+import com.evva.xesar.abrevva.nfc.Message
+import com.evva.xesar.abrevva.nfc.Mqtt5Client
+import com.evva.xesar.abrevva.nfc.NfcDelegate
+import com.evva.xesar.abrevva.nfc.asByteArray
+import com.evva.xesar.abrevva.nfc.toHexString
+import com.facebook.react.bridge.BaseActivityEventListener
+import com.facebook.react.bridge.LifecycleEventListener
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish
+import java.util.Timer
+import java.util.TimerTask
+
+class AbrevvaNfcModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ private val host = "172.16.2.91"
+ private val port = 1883
+ private val clientID = "96380897-0eee-479e-80c3-84c0dde286cd"
+
+ private val STATUS_NFC_OK = "enabled"
+
+ private val kyOffTimer = Timer()
+ private val hbTimer = Timer()
+
+ private var mqtt5Client: Mqtt5Client? = null
+ private var nfcDelegate = NfcDelegate()
+
+ private var clientId: String? = null
+
+ private val adapterStatus: String
+ get() = nfcDelegate.setAdapterStatus()
+
+ private val activityEventListener = object : BaseActivityEventListener() {
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ if (intent != null) {
+ currentActivity!!.intent = intent
+ nfcDelegate.processTag(intent) {
+ mqtt5Client?.subscribe("readers/1/$clientId/t", ::messageReceivedCallback)
+ mqtt5Client?.publish(
+ "readers/1/$clientId",
+ Message(
+ "ky",
+ "on",
+ nfcDelegate.getIdentifier(),
+ nfcDelegate.getHistoricalBytesAsHexString(),
+ "BAKA"
+ ).asByteArray()
+ )
+ setDisconnectTimer()
+ setHbTimer()
+ }
+ }
+ }
+ }
+
+ private val lifecycleEventListener = object : LifecycleEventListener {
+ override fun onHostResume() {
+ nfcDelegate.restartForegroundDispatch(reactContext, currentActivity)
+ }
+
+ override fun onHostPause() {
+ nfcDelegate.disableForegroundDispatch(reactContext, currentActivity)
+ }
+
+ override fun onHostDestroy() {
+ }
+ }
+
+ init {
+ reactContext.addActivityEventListener(activityEventListener)
+ reactContext.addLifecycleEventListener(lifecycleEventListener)
+ nfcDelegate.setAdapter(NfcAdapter.getDefaultAdapter(reactContext))
+ }
+
+ private fun messageReceivedCallback(response: Mqtt5Publish) {
+ try {
+ val resp = nfcDelegate.transceive(response.payloadAsBytes)
+ mqtt5Client?.publish("readers/1/$clientId/f", resp)
+ } catch (e: Exception) {
+ println(e)
+ }
+ }
+
+ private fun setDisconnectTimer() {
+ kyOffTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ try {
+ // .isConnected throws SecurityException when Tag is outdated
+ nfcDelegate.isConnected()
+ } catch (ex: java.lang.Exception) {
+ mqtt5Client?.publish(
+ "readers/1",
+ Message("ky", "off", oid = clientId).asByteArray()
+ )
+ this.cancel()
+ }
+ }
+ }, 250, 250)
+ }
+
+ private fun setHbTimer() {
+ hbTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ mqtt5Client?.publish("readers/1", Message("cr", "hb", oid = clientId).asByteArray())
+ }
+ }, 30000, 30000)
+ }
+
+ @ReactMethod
+ fun read(promise: Promise) {
+ if (adapterStatus != STATUS_NFC_OK) {
+ // No NFC hardware or NFC is disabled by the user
+ promise.reject(adapterStatus)
+ return
+ }
+ nfcDelegate.restartForegroundDispatch(reactApplicationContext, currentActivity)
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ @ReactMethod
+ fun connect() {
+ val ksh = KeyStoreHandler()
+ try {
+ val cacheDir = reactApplicationContext.cacheDir
+ ksh.parseP12File("$cacheDir/client-android.p12", "123")
+ ksh.initKeyManagerFactory()
+ ksh.initTrustManagerFactory()
+ } catch (ex: Exception) {
+ println(ex)
+ return
+ }
+
+ this.clientId = clientID
+ this.mqtt5Client = Mqtt5Client(clientID, port, host, ksh)
+ mqtt5Client?.connect()
+ print(Message("ky", "off", oid = "oidValue").asByteArray().toHexString())
+ }
+
+ @ReactMethod
+ fun disconnect() {
+ hbTimer.cancel()
+ kyOffTimer.cancel()
+ mqtt5Client?.publish("readers/1", Message("cr", "off", oid = clientID).asByteArray())
+ mqtt5Client?.disconnect()
+ }
+
+ override fun getName(): String {
+ return NAME
+ }
+
+ companion object {
+ const val NAME = "AbrevvaNfc"
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/evva/xesar/abrevva/reactnative/ExampleAppPackage.kt b/android/src/main/java/com/evva/xesar/abrevva/reactnative/ExampleAppPackage.kt
new file mode 100644
index 0000000..7810076
--- /dev/null
+++ b/android/src/main/java/com/evva/xesar/abrevva/reactnative/ExampleAppPackage.kt
@@ -0,0 +1,18 @@
+package com.evva.xesar.abrevva.reactnative
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class ExampleAppPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(AbrevvaCryptoModule(reactContext)) + listOf(AbrevvaNfcModule(reactContext)) + listOf(
+ AbrevvaBleModule(reactContext)
+ )
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/exampleapp/AbrevvaNfcModule.kt b/android/src/main/java/com/exampleapp/AbrevvaNfcModule.kt
deleted file mode 100644
index 5a7a01d..0000000
--- a/android/src/main/java/com/exampleapp/AbrevvaNfcModule.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-package com.exampleapp
-
-import android.app.Activity
-import android.content.Intent
-import android.nfc.NfcAdapter
-import android.os.Build
-import androidx.annotation.RequiresApi
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.bridge.ReactContextBaseJavaModule
-import com.facebook.react.bridge.ReactMethod
-import com.facebook.react.bridge.Promise
-import com.evva.xesar.abrevva.nfc.KeyStoreHandler
-import com.evva.xesar.abrevva.nfc.Message
-import com.evva.xesar.abrevva.nfc.Mqtt5Client
-import com.evva.xesar.abrevva.nfc.NfcDelegate
-import com.evva.xesar.abrevva.nfc.asByteArray
-import com.evva.xesar.abrevva.nfc.toHexString
-import com.facebook.react.bridge.ActivityEventListener
-import com.facebook.react.bridge.BaseActivityEventListener
-import com.facebook.react.bridge.LifecycleEventListener
-import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish
-import java.util.Timer
-import java.util.TimerTask
-
-class AbrevvaNfcModule(reactContext: ReactApplicationContext) :
- ReactContextBaseJavaModule(reactContext) {
-
- private val host = "172.16.2.91"
- private val port = 1883
- private val clientID = "96380897-0eee-479e-80c3-84c0dde286cd"
-
- private val STATUS_NFC_OK = "enabled"
-
- private val kyOffTimer = Timer()
- private val hbTimer = Timer()
-
- private var mqtt5Client: Mqtt5Client? = null
- private var nfcDelegate = NfcDelegate()
-
- private var clientId: String? = null
-
- private val adapterStatus: String
- get() = nfcDelegate.setAdapterStatus()
-
- private val activityEventListener = object : BaseActivityEventListener() {
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- if (intent != null) {
- currentActivity!!.intent = intent
- nfcDelegate.processTag(intent) {
- mqtt5Client?.subscribe("readers/1/$clientId/t", ::messageReceivedCallback)
- mqtt5Client?.publish(
- "readers/1/$clientId",
- Message(
- "ky",
- "on",
- nfcDelegate.getIdentifier(),
- nfcDelegate.getHistoricalBytesAsHexString(),
- "BAKA"
- ).asByteArray()
- )
- setDisconnectTimer()
- setHbTimer()
- }
- }
- }
- }
-
- private val lifecycleEventListener = object : LifecycleEventListener {
- override fun onHostResume() {
- nfcDelegate.restartForegroundDispatch(reactContext, currentActivity)
- }
-
- override fun onHostPause() {
- nfcDelegate.disableForegroundDispatch(reactContext, currentActivity)
- }
-
- override fun onHostDestroy() {
- }
- }
-
- init {
- reactContext.addActivityEventListener(activityEventListener)
- reactContext.addLifecycleEventListener(lifecycleEventListener)
- nfcDelegate.setAdapter(NfcAdapter.getDefaultAdapter(reactContext))
- }
-
- private fun messageReceivedCallback(response: Mqtt5Publish) {
- try {
- val resp = nfcDelegate.transceive(response.payloadAsBytes)
- mqtt5Client?.publish("readers/1/$clientId/f", resp)
- } catch (e: Exception){
- println(e)
- }
- }
-
- private fun setDisconnectTimer() {
- kyOffTimer.scheduleAtFixedRate(object : TimerTask() {
- override fun run() {
- try {
- // .isConnected throws SecurityException when Tag is outdated
- nfcDelegate.isConnected()
- } catch (ex: java.lang.Exception) {
- mqtt5Client?.publish("readers/1",Message("ky", "off", oid = clientId).asByteArray())
- this.cancel()
- }
- }
- }, 250, 250)
- }
-
- private fun setHbTimer(){
- hbTimer.scheduleAtFixedRate(object : TimerTask() {
- override fun run() {
- mqtt5Client?.publish("readers/1", Message("cr", "hb", oid = clientId).asByteArray())
- }
- }, 30000, 30000)
- }
-
- @ReactMethod
- fun read(promise: Promise) {
- if (adapterStatus != STATUS_NFC_OK) {
- // No NFC hardware or NFC is disabled by the user
- promise.reject(adapterStatus)
- return
- }
- nfcDelegate.restartForegroundDispatch(reactApplicationContext, currentActivity)
- }
-
- @OptIn(ExperimentalStdlibApi::class)
- @ReactMethod
- fun connect() {
- val ksh = KeyStoreHandler()
- try {
- val cacheDir = reactApplicationContext.cacheDir
- ksh.parseP12File("$cacheDir/client-android.p12", "123")
- ksh.initKeyManagerFactory()
- ksh.initTrustManagerFactory()
- }
- catch (ex: Exception) {
- println(ex)
- return
- }
-
- this.clientId = clientID
- this.mqtt5Client = Mqtt5Client(clientID, port, host, ksh)
- mqtt5Client?.connect()
- print(Message("ky", "off", oid = "oidValue").asByteArray().toHexString())
- }
-
- @ReactMethod
- fun disconnect() {
- hbTimer.cancel()
- kyOffTimer.cancel()
- mqtt5Client?.publish("readers/1",Message("cr", "off", oid = clientID).asByteArray())
- mqtt5Client?.disconnect()
- }
-
- override fun getName(): String {
- return NAME
- }
- companion object {
- const val NAME = "AbrevvaNfc"
- }
-}
\ No newline at end of file
diff --git a/android/src/main/java/com/exampleapp/ExampleAppPackage.kt b/android/src/main/java/com/exampleapp/ExampleAppPackage.kt
deleted file mode 100644
index c1a9b31..0000000
--- a/android/src/main/java/com/exampleapp/ExampleAppPackage.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.exampleapp
-
-import com.facebook.react.ReactPackage
-import com.facebook.react.bridge.NativeModule
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.uimanager.ViewManager
-
-
-class ExampleAppPackage : ReactPackage {
- override fun createNativeModules(reactContext: ReactApplicationContext): List {
- return listOf(AbrevvaCryptoModule(reactContext)) + listOf(AbrevvaNfcModule(reactContext)) + listOf(AbrevvaBleModule(reactContext))
- }
-
- override fun createViewManagers(reactContext: ReactApplicationContext): List> {
- return emptyList()
- }
-}
diff --git a/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModuleTest.kt b/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModuleTest.kt
new file mode 100644
index 0000000..9124527
--- /dev/null
+++ b/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaBleModuleTest.kt
@@ -0,0 +1,36 @@
+package com.evva.xesar.abrevva.reactnative
+
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.MockK
+import io.mockk.unmockkAll
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+
+class AbrevvaBleModuleTest {
+ private lateinit var abrevvaBleModule: com.evva.xesar.abrevva.reactnative.AbrevvaBleModule
+
+ private lateinit var testMap: WritableMapTestImplementation
+
+ @MockK(relaxed = true)
+ private lateinit var contextMock: ReactApplicationContext
+
+ @MockK(relaxed = true)
+ private lateinit var promiseMock: Promise
+
+ @MockK(relaxed = true)
+ private lateinit var readableMapMock: ReadableMap
+
+ @BeforeEach
+ fun beforeEach() {
+ MockKAnnotations.init(this)
+ abrevvaBleModule = com.evva.xesar.abrevva.reactnative.AbrevvaBleModule(contextMock)
+ }
+
+ @AfterEach
+ fun afterEach() {
+ unmockkAll()
+ }
+}
\ No newline at end of file
diff --git a/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModuleTest.kt b/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModuleTest.kt
new file mode 100644
index 0000000..769eac4
--- /dev/null
+++ b/android/src/test/java/com/evva/xesar/abrevva/reactnative/AbrevvaCryptoModuleTest.kt
@@ -0,0 +1,392 @@
+package com.evva.xesar.abrevva.reactnative
+
+import com.evva.xesar.abrevva.crypto.AesCCM
+import com.evva.xesar.abrevva.crypto.AesGCM
+import com.evva.xesar.abrevva.crypto.HKDF
+import com.evva.xesar.abrevva.crypto.SimpleSecureRandom
+import com.evva.xesar.abrevva.crypto.X25519Wrapper
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.WritableMap
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import org.bouncycastle.util.encoders.Hex
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+import org.junit.jupiter.params.provider.Arguments as JunitArguments
+
+class AbrevvaCryptoModuleTest {
+
+ private lateinit var abrevvaCryptoModule: com.evva.xesar.abrevva.reactnative.AbrevvaCryptoModule
+
+ private lateinit var testMap: WritableMapTestImplementation
+
+ @MockK(relaxed = true)
+ private lateinit var contextMock: ReactApplicationContext
+
+ @MockK(relaxed = true)
+ private lateinit var promiseMock: Promise
+
+ @MockK(relaxed = true)
+ private lateinit var readableMapMock: ReadableMap
+
+ @BeforeEach
+ fun beforeEach() {
+ MockKAnnotations.init(this)
+ mockkObject(AesCCM)
+ mockkObject(AesGCM)
+ mockkObject(X25519Wrapper)
+ mockkObject(SimpleSecureRandom)
+ mockkObject(HKDF)
+ mockkStatic(Arguments::createMap)
+ mockkStatic(Hex::class)
+ testMap = WritableMapTestImplementation()
+ every { Arguments.createMap() } returns testMap
+ every { Hex.decode(any()) } returns byteArrayOf(1)
+ abrevvaCryptoModule = com.evva.xesar.abrevva.reactnative.AbrevvaCryptoModule(contextMock)
+ }
+
+ @AfterEach
+ fun afterEach() {
+ unmockkAll()
+ }
+
+ @Nested
+ @DisplayName("encrypt()")
+ inner class EncryptTests {
+ @Test
+ fun `should reject if ct is empty`() {
+ every { Hex.decode(any()) } answers { callOriginal() }
+ every { AesCCM.encrypt(any(), any(), any(), any(), any()) } returns ByteArray(0)
+
+ abrevvaCryptoModule.encrypt(readableMapMock, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ @Test
+ fun `should resolve if ct is not empty`() {
+ every { AesCCM.encrypt(any(), any(), any(), any(), any()) } returns ByteArray(10)
+
+ abrevvaCryptoModule.encrypt(readableMapMock, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("decrypt()")
+ inner class DecryptTests {
+ @Test
+ fun `should reject if pt is empty`() {
+ every { AesCCM.decrypt(any(), any(), any(), any(), any()) } returns ByteArray(0)
+
+ abrevvaCryptoModule.decrypt(readableMapMock, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ @Test
+ fun `should resolve if pt is not empty`() {
+ every { AesCCM.decrypt(any(), any(), any(), any(), any()) } returns ByteArray(10)
+
+ abrevvaCryptoModule.decrypt(readableMapMock, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("generateKeyPair()")
+ inner class GenerateKeyPairTests {
+ @Test
+ fun `should resolve if keys where generated successfully`() {
+ every { X25519Wrapper.generateKeyPair() } returns mockk(relaxed = true)
+
+ abrevvaCryptoModule.generateKeyPair(promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+
+ @Test
+ fun `should reject if keys cannot be generated`() {
+ every { X25519Wrapper.generateKeyPair() } throws Exception("generateKeyPair() Fail Exception")
+
+ abrevvaCryptoModule.generateKeyPair(promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("encryptFile()")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ inner class EncryptFileTests {
+ @ParameterizedTest(name = "encryptFile({0}, {1}, {2}) should reject")
+ @MethodSource("parameterizedArgs_encrypt")
+ fun `encryptFile() should reject if any Param is missing`(
+ ctPath: String?,
+ ptPath: String?,
+ sharedSecret: String?
+ ) {
+ testMap.putString("ctPath", ctPath)
+ testMap.putString("ptPath", ptPath)
+ testMap.putString("sharedSecret", sharedSecret)
+
+ abrevvaCryptoModule.encryptFile(testMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ fun parameterizedArgs_encrypt(): Stream {
+ return Stream.of(
+ JunitArguments.of("", "ptPath", "sharedSecret"),
+ JunitArguments.of("ctPath", "", "sharedSecret"),
+ JunitArguments.of("ctPath", "sharedSecret", ""),
+ JunitArguments.of(null, "ptPath", "sharedSecret"),
+ JunitArguments.of("ctPath", null, "sharedSecret"),
+ JunitArguments.of("ctPath", "ptPath", null),
+ )
+ }
+
+ @Test
+ fun `should resolve if args are valid and file could be encrypted`() {
+ val mapMock = mockk(relaxed = true)
+ every { mapMock.getString(any()) } returns "notEmpty"
+ every { AesGCM.encryptFile(any(), any(), any()) } returns true
+
+ abrevvaCryptoModule.encryptFile(mapMock, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+
+ @Test
+ fun `should reject if args are valid but encryption fails`() {
+ val mapMock = mockk(relaxed = true)
+ every { mapMock.getString(any()) } returns "notEmpty"
+ every {
+ AesGCM.encryptFile(
+ any(),
+ any(),
+ any()
+ )
+ } throws Exception("encryptFile() Fail Exception")
+
+ abrevvaCryptoModule.encryptFile(mapMock, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("decryptFile()")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ inner class DecryptFileTests {
+ @ParameterizedTest(name = "empty args should be rejected")
+ @MethodSource("parameterizedArgs_decrypt")
+ fun `should reject if any Param is empty`(
+ ctPath: String?,
+ ptPath: String?,
+ sharedSecret: String?
+ ) {
+ testMap.putString("ctPath", ctPath)
+ testMap.putString("ptPath", ptPath)
+ testMap.putString("sharedSecret", sharedSecret)
+
+ abrevvaCryptoModule.decryptFile(testMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ fun parameterizedArgs_decrypt(): Stream {
+ return Stream.of(
+ JunitArguments.of("", "ptPath", "sharedSecret"),
+ JunitArguments.of("ctPath", "", "sharedSecret"),
+ JunitArguments.of("ctPath", "ptPath", ""),
+ JunitArguments.of(null, "ptPath", "sharedSecret"),
+ JunitArguments.of("ctPath", null, "sharedSecret"),
+ JunitArguments.of("ctPath", "ptPath", null),
+ )
+ }
+
+ @Test
+ fun `should resolve if args are valid and file could be encrypted`() {
+ val mapMock = mockk(relaxed = true)
+ every { mapMock.getString(any()) } returns "notEmpty"
+ every { AesGCM.decryptFile(any(), any(), any()) } returns true
+
+ abrevvaCryptoModule.decryptFile(mapMock, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+
+ @Test
+ fun `should reject if encryption fails`() {
+ val mapMock = mockk(relaxed = true)
+ every { mapMock.getString(any()) } returns "notEmpty"
+ every {
+ AesGCM.decryptFile(
+ any(),
+ any(),
+ any()
+ )
+ } throws Exception("encryptFile() Fail Exception")
+
+ abrevvaCryptoModule.decryptFile(mapMock, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("decryptFileFromURL()")
+ inner class DecryptFileFromURLTests {
+
+ @Nested
+ @DisplayName("should reject if any Param is empty")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ inner class DecryptFileFromURL_ParameterizedTest {
+ @ParameterizedTest
+ @MethodSource("parameterizedArgs_decryptFileFromURL")
+ fun `should reject if any Param is empty`(
+ sharedSecret: String?,
+ url: String?,
+ ptPath: String?
+ ) {
+ testMap.putString("sharedSecret", sharedSecret)
+ testMap.putString("url", url)
+ testMap.putString("ptPath", ptPath)
+
+ abrevvaCryptoModule.decryptFileFromURL(testMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ fun parameterizedArgs_decryptFileFromURL(): Stream {
+ return Stream.of(
+ JunitArguments.of("", "url", "ptPath"),
+ JunitArguments.of("sharedSecret", "", "ptPath"),
+ JunitArguments.of("sharedSecret", "url", ""),
+ JunitArguments.of(null, "url", "ptPath"),
+ JunitArguments.of("sharedSecret", null, "ptPath"),
+ JunitArguments.of("sharedSecret", "url", null),
+ )
+ }
+ }
+
+ @Test
+ fun `decryptFileFromURL() should reject if ctPath-File is not accessible`() {
+ val mockMap = mockk(relaxed = true)
+ val moduleSpy =
+ spyk(com.evva.xesar.abrevva.reactnative.AbrevvaCryptoModule(contextMock))
+ every { mockMap.getString(any()) } returns "notEmpty"
+ every {
+ moduleSpy.writeToFile(
+ any(),
+ any()
+ )
+ } throws Exception("decryptFileFromURL() Fail Exception")
+
+ moduleSpy.decryptFileFromURL(mockMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ @Test
+ fun `decryptFileFromURL() should reject if decode fails`() {
+ val mockMap = mockk(relaxed = true)
+ val moduleSpy =
+ spyk(com.evva.xesar.abrevva.reactnative.AbrevvaCryptoModule(contextMock))
+ every { mockMap.getString(any()) } returns "notEmpty"
+ every { moduleSpy.writeToFile(any(), any()) } returns Unit
+ every { Hex.decode(any()) } throws Exception("decryptFileFromURL() Fail Exception")
+
+ moduleSpy.decryptFileFromURL(mockMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ @Test
+ fun `decryptFileFromURL() should resolve if everything works as intended`() {
+ val mockMap = mockk(relaxed = true)
+ val moduleSpy =
+ spyk(com.evva.xesar.abrevva.reactnative.AbrevvaCryptoModule(contextMock))
+ every { mockMap.getString(any()) } returns "notEmpty"
+ every { moduleSpy.writeToFile(any(), any()) } returns Unit
+ every { AesGCM.decryptFile(any(), any(), any()) } returns true
+
+ moduleSpy.decryptFileFromURL(mockMap, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("random()")
+ inner class RandomTests {
+ @ParameterizedTest(name = "random(numBytes: {0}) resolved String size should be {1}")
+ @CsvSource("2,4", "4,8", "7,14")
+ fun `should return random bytes n number of bytes if successful`(
+ numBytes: Int,
+ expectedStrLen: Int
+ ) {
+ val mockMap = mockk(relaxed = true)
+ every { mockMap.getInt("numBytes") } returns numBytes
+
+ abrevvaCryptoModule.random(mockMap, promiseMock)
+
+ assert(testMap.getString("value")!!.length == expectedStrLen)
+ }
+
+ @Test
+ fun `should reject if bytes cannot be generated`() {
+ every { SimpleSecureRandom.getSecureRandomBytes(any()) } returns ByteArray(0)
+ testMap.putInt("numBytes", 10)
+
+ abrevvaCryptoModule.random(testMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+ }
+
+ @Nested
+ @DisplayName("derive()")
+ inner class DeriveTests {
+
+ @Test
+ fun `should resolve if successful`() {
+ testMap.putInt("length", 0)
+ every { HKDF.derive(any(), any(), any(), any()) } returns ByteArray(0)
+
+ abrevvaCryptoModule.derive(testMap, promiseMock)
+
+ verify { promiseMock.reject(any()) }
+ }
+
+ @Test
+ fun `should reject if unsuccessful`() {
+ testMap.putInt("length", 10)
+ every { HKDF.derive(any(), any(), any(), any()) } returns ByteArray(10)
+ abrevvaCryptoModule.derive(testMap, promiseMock)
+
+ verify { promiseMock.resolve(any()) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/test/java/com/evva/xesar/abrevva/reactnative/WriteableMapTestImplementation.kt b/android/src/test/java/com/evva/xesar/abrevva/reactnative/WriteableMapTestImplementation.kt
new file mode 100644
index 0000000..8fd086e
--- /dev/null
+++ b/android/src/test/java/com/evva/xesar/abrevva/reactnative/WriteableMapTestImplementation.kt
@@ -0,0 +1,101 @@
+package com.evva.xesar.abrevva.reactnative
+
+import com.facebook.react.bridge.Dynamic
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.ReadableMapKeySetIterator
+import com.facebook.react.bridge.ReadableType
+import com.facebook.react.bridge.WritableMap
+import io.mockk.mockk
+
+class WritableMapTestImplementation : WritableMap {
+ private val map = mutableMapOf()
+
+ override fun hasKey(p0: String): Boolean {
+ return map.containsKey(p0)
+ }
+
+ override fun isNull(p0: String): Boolean {
+ return map[p0] == null
+ }
+
+ override fun getBoolean(p0: String): Boolean {
+ return map[p0] as Boolean
+ }
+
+ override fun getDouble(p0: String): Double {
+ return map[p0] as Double
+ }
+
+ override fun getInt(p0: String): Int {
+ return map[p0] as Int
+ }
+
+ override fun getString(p0: String): String? {
+ return map[p0] as String?
+ }
+
+ override fun getArray(p0: String): ReadableArray? {
+ return map[p0] as ReadableArray?
+ }
+
+ override fun getMap(p0: String): ReadableMap? {
+ return map[p0] as ReadableMap?
+ }
+
+ override fun getDynamic(p0: String): Dynamic {
+ return mockk()
+ }
+
+ override fun getType(p0: String): ReadableType {
+ return mockk()
+ }
+
+ override fun getEntryIterator(): MutableIterator> {
+ return mockk>>()
+ }
+
+ override fun keySetIterator(): ReadableMapKeySetIterator {
+ return mockk()
+ }
+
+ override fun toHashMap(): HashMap {
+ return mockk>()
+ }
+
+ override fun putNull(p0: String) {
+ map[p0] = null
+ }
+
+ override fun putBoolean(p0: String, p1: Boolean) {
+ map[p0] = p1
+ }
+
+ override fun putDouble(p0: String, p1: Double) {
+ map[p0] = p1
+ }
+
+ override fun putInt(p0: String, p1: Int) {
+ map[p0] = p1
+ }
+
+ override fun putString(p0: String, p1: String?) {
+ map[p0] = p1
+ }
+
+ override fun putArray(p0: String, p1: ReadableArray?) {
+ map[p0] = p1
+ }
+
+ override fun putMap(p0: String, p1: ReadableMap?) {
+ map[p0] = p1
+ }
+
+ override fun merge(p0: ReadableMap) {
+ TODO("Not yet implemented")
+ }
+
+ override fun copy(): WritableMap {
+ TODO("Not yet implemented")
+ }
+}
\ No newline at end of file
diff --git a/bob.config.js b/bob.config.js
index 3e4f000..6d53e0e 100644
--- a/bob.config.js
+++ b/bob.config.js
@@ -1,24 +1,24 @@
module.exports = {
- "source": "src",
- "output": "lib",
- "targets": [
+ source: 'src',
+ output: 'lib',
+ targets: [
[
- "commonjs",
+ 'commonjs',
{
- "esm": true
- }
+ esm: true,
+ },
],
[
- "module",
+ 'module',
{
- "esm": true
- }
+ esm: true,
+ },
],
[
- "typescript",
+ 'typescript',
{
- "project": "tsconfig.build.json"
- }
- ]
- ]
-};
\ No newline at end of file
+ project: 'tsconfig.build.json',
+ },
+ ],
+ ],
+};
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 5a29035..eb54c35 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -104,7 +104,7 @@ android {
}
packagingOptions {
resources {
- excludes += ['META-INF/INDEX.LIST', 'META-INF/io.netty.versions.properties', 'META-INF/*.properties','META-INF/INDEX.LIST']
+ excludes += ['META-INF/INDEX.LIST', 'META-INF/io.netty.versions.properties', 'META-INF/*.properties', 'META-INF/INDEX.LIST']
}
}
}
@@ -126,20 +126,11 @@ repositories {
google()
mavenCentral()
mavenLocal()
- maven {
- name = 'evva-maven'
- url = uri('https://maven.pkg.github.com/evva-sfw/abrevva-sdk-android')
- credentials {
- username = properties.getProperty('github.username') ?: System.getenv('GH_USERNAME')
- password = properties.getProperty('github.token') ?: System.getenv('GH_TOKEN')
- }
- }
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
- implementation group: "com.evva.xesar", name: "abrevva-sdk-android", version: "1.0.15"
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar
index 7f93135..d64cd49 100644
Binary files a/example/android/gradle/wrapper/gradle-wrapper.jar and b/example/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 2ea3535..e6aba25 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat
index 7101f8e..6689b85 100644
--- a/example/android/gradlew.bat
+++ b/example/android/gradlew.bat
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
goto fail
diff --git a/example/index.js b/example/index.js
index 117ddca..e77bb4a 100644
--- a/example/index.js
+++ b/example/index.js
@@ -1,5 +1,6 @@
import { AppRegistry } from 'react-native';
-import App from './src/App';
+
import { name as appName } from './app.json';
+import App from './src/App';
AppRegistry.registerComponent(appName, () => App);
diff --git a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/UserInterfaceState.xcuserstate b/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index 6e713f1..0000000
Binary files a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
deleted file mode 100644
index f320115..0000000
--- a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/matthias.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/palic.xcuserdatad/UserInterfaceState.xcuserstate b/example/ios/ExampleAppExample.xcworkspace/xcuserdata/palic.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index 64480bd..0000000
Binary files a/example/ios/ExampleAppExample.xcworkspace/xcuserdata/palic.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/example/ios/Podfile b/example/ios/Podfile
index a4e60ef..1a4e86f 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -8,7 +8,6 @@ require Pod::Executable.execute_command('node', ['-p',
platform :ios, '15.0'
prepare_react_native_project!
-source 'https://github.com/evva-sfw/abrevva-sdk-ios-pod-specs.git'
source 'https://cdn.cocoapods.org/'
# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 23a7b14..1b09c9d 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,5 +1,29 @@
PODS:
- - AbrevvaSDK (1.0.15):
+ - abrevva-react-native (0.1.0):
+ - AbrevvaSDK (~> 1.0.20)
+ - CocoaMQTT
+ - CryptoSwift
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Codegen
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - AbrevvaSDK (1.0.20):
- CocoaMQTT
- CryptoSwift
- boost (1.83.0)
@@ -7,7 +31,7 @@ PODS:
- CocoaMQTT/Core (= 2.1.6)
- CocoaMQTT/Core (2.1.6):
- MqttCocoaAsyncSocket (~> 1.0.8)
- - CryptoSwift (1.8.2)
+ - CryptoSwift (1.8.3)
- DoubleConversion (1.1.6)
- FBLazyVector (0.74.3)
- fmt (9.1.0)
@@ -944,30 +968,6 @@ PODS:
- React-Mapbuffer (0.74.3):
- glog
- React-debug
- - react-native-example-app (0.1.0):
- - AbrevvaSDK (= 1.0.15)
- - CocoaMQTT
- - CryptoSwift
- - DoubleConversion
- - glog
- - hermes-engine
- - RCT-Folly (= 2024.01.01.00)
- - RCTRequired
- - RCTTypeSafety
- - React-Codegen
- - React-Core
- - React-debug
- - React-Fabric
- - React-featureflags
- - React-graphics
- - React-ImageManager
- - React-NativeModulesApple
- - React-RCTFabric
- - React-rendererdebug
- - React-utils
- - ReactCommon/turbomodule/bridging
- - ReactCommon/turbomodule/core
- - Yoga
- react-native-safe-area-context (4.10.8):
- React-Core
- React-nativeconfig (0.74.3)
@@ -1201,7 +1201,7 @@ PODS:
- React-utils (= 0.74.3)
- RNFS (2.20.0):
- React-Core
- - RNScreens (3.32.0):
+ - RNScreens (3.34.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1227,6 +1227,7 @@ PODS:
- Yoga (0.0.0)
DEPENDENCIES:
+ - abrevva-react-native (from `../..`)
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
@@ -1259,7 +1260,6 @@ DEPENDENCIES:
- React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- - react-native-example-app (from `../..`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
@@ -1289,15 +1289,16 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
- https://github.com/evva-sfw/abrevva-sdk-ios-pod-specs.git:
- - AbrevvaSDK
trunk:
+ - AbrevvaSDK
- CocoaMQTT
- CryptoSwift
- MqttCocoaAsyncSocket
- SocketRocket
EXTERNAL SOURCES:
+ abrevva-react-native:
+ :path: "../.."
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
@@ -1359,8 +1360,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/logger"
React-Mapbuffer:
:path: "../node_modules/react-native/ReactCommon"
- react-native-example-app:
- :path: "../.."
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
React-nativeconfig:
@@ -1417,10 +1416,11 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- AbrevvaSDK: 933f1d13612f7984a903a5c8115b34d3e68d1692
+ abrevva-react-native: a71c4e72921fc2f516bc32d9818839b48842f02f
+ AbrevvaSDK: fa79760425864800f524905612f708fa9e323fc5
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
CocoaMQTT: 1f206228b29318eabdacad0c2e4e88575922c27a
- CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea
+ CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
@@ -1451,7 +1451,6 @@ SPEC CHECKSUMS:
React-jsitracing: 1aa5681c353b41573b03e0e480a5adf5fa1c56d8
React-logger: fa92ba4d3a5d39ac450f59be2a3cec7b099f0304
React-Mapbuffer: 70da5955150a58732e9336a0c7e71cd49e909f25
- react-native-example-app: 5f4e6cf425690c11a463d60a3122a641b7020d7e
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
React-nativeconfig: 84806b820491db30175afbf027e99e8415dc63f0
React-NativeModulesApple: 7b79212f8cf496ab554e0b7b09acbd4aa4690260
@@ -1477,10 +1476,10 @@ SPEC CHECKSUMS:
React-utils: 6f7ac39d9a0de447d4334bb25d144a28c0c5d8c9
ReactCommon: 4a09c7d8a06e93c1e2e988a3b9f3db3d2449f2fc
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
- RNScreens: fd2722bcc59f36a629205af8cc7b48e4bc0d09f5
+ RNScreens: db442e7b8c7bc8befec2ce057927305ff8598cc8
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: eed50599a85bd9f6882a9938d118aed6a397db9c
-PODFILE CHECKSUM: 305f8b38479ce74425909f150a0e67cf5d6eabfd
+PODFILE CHECKSUM: c096dde368bb25db14174c2f8e4a6b2095e483e8
COCOAPODS: 1.15.2
diff --git a/example/package.json b/example/package.json
index 4167943..720f7f2 100644
--- a/example/package.json
+++ b/example/package.json
@@ -1,5 +1,5 @@
{
- "name": "react-native-example-app-example",
+ "name": "abrevva-react-native-example",
"version": "0.0.1",
"private": true,
"scripts": {
diff --git a/example/src/App.tsx b/example/src/App.tsx
index cd72f0c..b3715c2 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,3 +1,4 @@
+import { AbrevvaCrypto, AbrevvaNfc } from '@evva-sfw/abrevva-react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useEffect } from 'react';
@@ -11,7 +12,6 @@ import {
TouchableOpacity,
View,
} from 'react-native';
-import { AbrevvaCrypto, AbrevvaNfc } from 'react-native-example-app';
import RNFS from 'react-native-fs';
import { BleScreen } from './BleScreenComponents';
diff --git a/example/src/BleScreenComponents.tsx b/example/src/BleScreenComponents.tsx
index 98eac83..8b0ff17 100644
--- a/example/src/BleScreenComponents.tsx
+++ b/example/src/BleScreenComponents.tsx
@@ -1,3 +1,13 @@
+import {
+ AbrevvaBle,
+ AbrevvaCrypto,
+ dataViewToHexString,
+ dataViewToNumbers,
+ hexStringToDataView,
+ numbersToDataView,
+ type ReadResult,
+ type ScanResult,
+} from '@evva-sfw/abrevva-react-native';
import { hex } from '@scure/base';
import { Parser } from 'binary-parser-encoder';
import { useEffect } from 'react';
@@ -12,16 +22,6 @@ import {
Text,
TouchableOpacity,
} from 'react-native';
-import {
- AbrevvaBle,
- AbrevvaCrypto,
- dataViewToHexString,
- dataViewToNumbers,
- hexStringToDataView,
- numbersToDataView,
- type ReadResult,
- type ScanResult,
-} from 'react-native-example-app';
global.Buffer = require('buffer').Buffer;
diff --git a/ios/ble/AbrevvaBle.swift b/ios/ble/AbrevvaBle.swift
index 206e08c..445573b 100644
--- a/ios/ble/AbrevvaBle.swift
+++ b/ios/ble/AbrevvaBle.swift
@@ -44,6 +44,7 @@ public class AbrevvaBle: RCTEventEmitter {
bleManager.registerStateReceiver { enabled in
self.sendEvent(withName: "onEnabledChanged", body: ["value": enabled])
}
+ resolve(nil)
}
@objc
diff --git a/jest.config.json b/jest.config.json
index 29a308b..f65a595 100644
--- a/jest.config.json
+++ b/jest.config.json
@@ -3,5 +3,8 @@
"modulePathIgnorePatterns": [
"/example/node_modules",
"/lib/"
+ ],
+ "setupFilesAfterEnv": [
+ "/src/setup.tsx"
]
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 185409f..20ff449 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "@evva-sfw/abrevva-react-native",
"version": "0.1.0",
+ "author": "EVVA Sicherheitstechnologie GmbH",
"description": "The EVVA React-Native Module is a collection of tools to work with electronical EVVA access components. It allows for scanning and connecting via BLE.",
"source": "./src/index.tsx",
"main": "./lib/commonjs/index.js",
@@ -37,7 +38,7 @@
"!**/.*"
],
"scripts": {
- "example": "yarn workspace react-native-example-app-example",
+ "example": "yarn workspace abrevva-react-native-example",
"test": "jest",
"typecheck": "tsc --noEmit",
"lint": "eslint \"**/*.{ts,tsx}\"",
diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx
deleted file mode 100644
index bf84291..0000000
--- a/src/__tests__/index.test.tsx
+++ /dev/null
@@ -1 +0,0 @@
-it.todo('write a test');
diff --git a/src/index.test.tsx b/src/index.test.tsx
new file mode 100644
index 0000000..3f13d95
--- /dev/null
+++ b/src/index.test.tsx
@@ -0,0 +1,248 @@
+import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
+
+import {
+ type AbrevvaBLEInterface,
+ AbrevvaCrypto,
+ AbrevvaNfc,
+ createAbrevvaBleInstance,
+} from './index';
+
+describe('AbrevvaBleModule', () => {
+ let AbrevvaBleMock = NativeModules.AbrevvaBle;
+ let AbrevvaBle: AbrevvaBLEInterface;
+ let mockEmitter: NativeEventEmitter;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+
+ Platform.OS = 'ios';
+ // @ts-ignore
+ Platform.select.mockImplementation(() => {
+ return mockEmitter;
+ });
+
+ mockEmitter = new NativeEventEmitter();
+ AbrevvaBle = createAbrevvaBleInstance();
+ jest.clearAllMocks();
+ });
+
+ it('constructor should throw if Platform is not Supported', () => {
+ // @ts-ignore
+ Platform.select.mockImplementation(() => {
+ return undefined;
+ });
+ expect(createAbrevvaBleInstance).toThrow();
+ });
+ it('should run initialize()', async () => {
+ await AbrevvaBle.initialize();
+ expect(AbrevvaBleMock.initialize).toHaveBeenCalledTimes(1);
+ });
+ it('should run isEnabled()', async () => {
+ await AbrevvaBle.isEnabled();
+ expect(AbrevvaBleMock.isEnabled).toHaveBeenCalledTimes(1);
+ });
+ it('should run isLocationEnabled()', async () => {
+ await AbrevvaBle.isLocationEnabled();
+ expect(AbrevvaBleMock.isLocationEnabled).toHaveBeenCalledTimes(1);
+ });
+
+ describe('startEnableNotification', () => {
+ it('should add the correct eventlistener and call startEnableNotification', async () => {
+ const spy = jest.spyOn(mockEmitter, 'addListener');
+ const spyNativeModule = jest.spyOn(AbrevvaBle, 'startEnabledNotifications');
+ const mockFn = jest.fn();
+ await AbrevvaBle.startEnabledNotifications(mockFn);
+
+ expect(spy).toHaveBeenCalledWith('onEnabledChanged', expect.any(Function));
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spyNativeModule).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('should run stopEnabledNotifications()', async () => {
+ await AbrevvaBle.stopEnabledNotifications();
+ expect(AbrevvaBleMock.stopEnabledNotifications).toHaveBeenCalledTimes(1);
+ });
+ it('should run openLocationSettings()', async () => {
+ await AbrevvaBle.openLocationSettings();
+ expect(AbrevvaBleMock.openLocationSettings).toHaveBeenCalledTimes(1);
+ });
+ it('should run openBluetoothSettings()', async () => {
+ await AbrevvaBle.openBluetoothSettings();
+ expect(AbrevvaBleMock.openBluetoothSettings).toHaveBeenCalledTimes(1);
+ });
+ it('should run openAppSettings()', async () => {
+ await AbrevvaBle.openAppSettings();
+ expect(AbrevvaBleMock.openAppSettings).toHaveBeenCalledTimes(1);
+ });
+
+ describe('requestLEScan()', () => {
+ it('should reject if a scan is already in progress', async () => {
+ void AbrevvaBle.requestLEScan(
+ () => {},
+ () => {},
+ () => {},
+ );
+ await expect(AbrevvaBle.requestLEScan).rejects.toThrow();
+ });
+ it('should add the expected eventlisteners and discard them after the timeout', async () => {
+ const addListenerSpy = jest.spyOn(mockEmitter, 'addListener');
+
+ const emitterSubscriptionMock = { remove: jest.fn() };
+ // @ts-ignore
+ mockEmitter.addListener.mockImplementation(() => {
+ return emitterSubscriptionMock;
+ });
+
+ void AbrevvaBle.requestLEScan(jest.fn(), jest.fn(), jest.fn());
+
+ jest.advanceTimersByTime(20000);
+
+ expect(addListenerSpy).toHaveBeenCalledWith('onScanResult', expect.any(Function));
+ expect(addListenerSpy).toHaveBeenCalledWith('onConnect', expect.any(Function));
+ expect(addListenerSpy).toHaveBeenCalledWith('onDisconnect', expect.any(Function));
+ expect(addListenerSpy).toHaveBeenCalledTimes(3);
+ expect(AbrevvaBleMock.requestLEScan).toHaveBeenCalledTimes(1);
+ expect(emitterSubscriptionMock.remove).toHaveBeenCalledTimes(3);
+ });
+ });
+ it('should run stopLEScan()', async () => {
+ await AbrevvaBle.stopLEScan();
+ expect(AbrevvaBleMock.stopLEScan).toHaveBeenCalledTimes(1);
+ });
+ it('should run connect()', async () => {
+ await AbrevvaBle.connect({ deviceId: 'deviceId' });
+ expect(AbrevvaBleMock.connect).toHaveBeenCalledTimes(1);
+ });
+ it('should run disconnect()', async () => {
+ await AbrevvaBle.disconnect({ deviceId: 'deviceId' });
+ expect(AbrevvaBleMock.disconnect).toHaveBeenCalledTimes(1);
+ expect(AbrevvaBleMock.setSupportedEvents).toHaveBeenCalledTimes(1);
+ });
+ it('should run read()', async () => {
+ await AbrevvaBle.read({
+ deviceId: 'deviceId',
+ service: 'service',
+ characteristic: 'characteristic',
+ });
+ expect(AbrevvaBleMock.read).toHaveBeenCalledTimes(1);
+ });
+ it('should run write()', async () => {
+ await AbrevvaBle.write({
+ deviceId: 'deviceId',
+ service: 'service',
+ characteristic: 'characteristic',
+ value: 'value',
+ });
+ expect(AbrevvaBleMock.write).toHaveBeenCalledTimes(1);
+ });
+ it('should run signalize()', async () => {
+ await AbrevvaBle.signalize({ deviceId: 'deviceId' });
+ expect(AbrevvaBleMock.signalize).toHaveBeenCalledTimes(1);
+ });
+ it('should run disengage()', async () => {
+ await AbrevvaBle.disengage({
+ deviceId: 'deviceId',
+ mobileId: 'mobileId',
+ mobileDeviceKey: 'mobileDeviceKey',
+ mobileGroupId: 'mobileGroupId',
+ mobileAccessData: 'mobileAccessData',
+ isPermanentRelease: false,
+ });
+ expect(AbrevvaBleMock.disengage).toHaveBeenCalledTimes(1);
+ });
+ describe('startNotifications()', () => {});
+ describe('stopNotifications()', () => {
+ const deviceId = 'deviceId';
+ const service = 'service';
+ const characteristic = 'characteristic';
+
+ let emitterSubscriptionMock: any;
+ beforeEach(() => {
+ emitterSubscriptionMock = { remove: jest.fn() };
+ void jest.spyOn(mockEmitter, 'addListener').mockImplementation(() => {
+ return emitterSubscriptionMock;
+ });
+ });
+ it('should delete the Eventlistener if it was previously added', async () => {
+ await AbrevvaBle.startNotifications(deviceId, service, characteristic, () => {});
+ expect(emitterSubscriptionMock.remove).toHaveBeenCalledTimes(0);
+ expect(AbrevvaBleMock.stopNotifications).toHaveBeenCalledTimes(0);
+ expect(AbrevvaBleMock.startNotifications).toHaveBeenCalledTimes(1);
+ await AbrevvaBle.stopNotifications(deviceId, service, characteristic);
+ expect(emitterSubscriptionMock.remove).toHaveBeenCalledTimes(1);
+ expect(AbrevvaBleMock.stopNotifications).toHaveBeenCalledTimes(1);
+ });
+ it("shouldn't remove any key if it wasn't previously added", async () => {
+ await AbrevvaBle.stopNotifications(deviceId, service, characteristic);
+ expect(emitterSubscriptionMock.remove).toHaveBeenCalledTimes(0);
+ expect(AbrevvaBleMock.stopNotifications).toHaveBeenCalledTimes(1);
+ });
+ });
+});
+
+describe('AbrevvaNfcModule', () => {
+ const AbrevvaNfcMock = NativeModules.AbrevvaNfc;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should run connect()', async () => {
+ await AbrevvaNfc.connect();
+ expect(AbrevvaNfcMock.connect).toHaveBeenCalledTimes(1);
+ });
+ it('should run disconnect()', async () => {
+ await AbrevvaNfc.disconnect();
+ expect(AbrevvaNfcMock.disconnect).toHaveBeenCalledTimes(1);
+ });
+ it('should run read()', async () => {
+ await AbrevvaNfc.read();
+ expect(AbrevvaNfcMock.read).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('AbrevvaCryptoModule', () => {
+ const AbrevvaCryptoMock = NativeModules.AbrevvaCrypto;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should run encrypt()', async () => {
+ await AbrevvaCrypto.encrypt('', '', '', '');
+ expect(AbrevvaCryptoMock.encrypt).toHaveBeenCalledTimes(1);
+ });
+ it('should run decrypt()', async () => {
+ await AbrevvaCrypto.decrypt('', '', '', '');
+ expect(AbrevvaCryptoMock.decrypt).toHaveBeenCalledTimes(1);
+ });
+ it('should run generateKeyPair()', async () => {
+ await AbrevvaCrypto.generateKeyPair();
+ expect(AbrevvaCryptoMock.generateKeyPair).toHaveBeenCalledTimes(1);
+ });
+ it('should run computeSharedSecret()', async () => {
+ await AbrevvaCrypto.computeSharedSecret('', '');
+ expect(AbrevvaCryptoMock.computeSharedSecret).toHaveBeenCalledTimes(1);
+ });
+ it('should run encryptFile()', async () => {
+ await AbrevvaCrypto.encryptFile('', '', '');
+ expect(AbrevvaCryptoMock.encryptFile).toHaveBeenCalledTimes(1);
+ });
+ it('should run decryptFile()', async () => {
+ await AbrevvaCrypto.decryptFile('', '', '');
+ expect(AbrevvaCryptoMock.decryptFile).toHaveBeenCalledTimes(1);
+ });
+ it('should run decryptFileFromURL()', async () => {
+ await AbrevvaCrypto.decryptFileFromURL('', '', '');
+ expect(AbrevvaCryptoMock.decryptFileFromURL).toHaveBeenCalledTimes(1);
+ });
+ it('should run random()', async () => {
+ await AbrevvaCrypto.random(0);
+ expect(AbrevvaCryptoMock.random).toHaveBeenCalledTimes(1);
+ });
+ it('should run derive()', async () => {
+ await AbrevvaCrypto.derive('', '', '', 0);
+ expect(AbrevvaCryptoMock.derive).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/index.tsx b/src/index.tsx
index aff6f56..63257b9 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,3 @@
-// https://stackoverflow.com/a/73382191
-// TODO: Verify that this works as intended
import {
DeviceEventEmitter,
type EmitterSubscription,
@@ -30,7 +28,7 @@ import type {
WriteOptions,
} from './interfaces';
-const NativeModuleNfc: AbrevvaNfcInterface = NativeModules.AbrevvaNfc
+const NativeModuleNfc = NativeModules.AbrevvaNfc
? NativeModules.AbrevvaNfc
: new Proxy(
{},
@@ -47,7 +45,7 @@ const NativeModuleCrypto = NativeModules.AbrevvaCrypto
{},
{
get() {
- throw new Error('Linking Error AbrevvaNfc');
+ throw new Error('Linking Error AbrevvaCrypto');
},
},
);
@@ -63,15 +61,18 @@ const NativeModuleBle = NativeModules.AbrevvaBle
},
);
-class AbrevvaBleModule implements AbrevvaBLEInterface {
+export class AbrevvaBleModule implements AbrevvaBLEInterface {
private listeners: Map;
- private eventEmitter: NativeEventEmitter | undefined;
+ private readonly eventEmitter: NativeEventEmitter | undefined;
constructor() {
this.eventEmitter = Platform.select({
ios: new NativeEventEmitter(NativeModuleBle),
android: DeviceEventEmitter,
});
+ if (this.eventEmitter === undefined) {
+ throw new Error('this platform is not supported');
+ }
this.listeners = new Map([
['onEnabledChanged', undefined],
@@ -80,7 +81,7 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
['onDisconnect', undefined],
]);
- NativeModuleBle.setSupportedEvents({ events: [...this.listeners.keys()] });
+ NativeModuleBle?.setSupportedEvents({ events: [...this.listeners.keys()] });
}
async initialize(androidNeverForLocation?: boolean): Promise {
@@ -98,13 +99,9 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
}
async startEnabledNotifications(callback: (result: boolean) => void): Promise {
- if (this.eventEmitter === undefined) {
- return Promise.reject('unsupported platform');
- }
-
this.listeners.set(
'onEnabledChanged',
- this.eventEmitter.addListener('onEnabledChanged', (event: any) => {
+ this.eventEmitter!.addListener('onEnabledChanged', (event: any) => {
callback(event.value);
}),
);
@@ -147,9 +144,6 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
scanMode?: ScanMode,
timeout?: number,
): Promise {
- if (this.eventEmitter === undefined) {
- return Promise.reject('unsupported platform');
- }
if (this.listeners.get('onScanResult') !== undefined) {
return Promise.reject('scan already in progress');
}
@@ -167,14 +161,14 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
};
const listeners = new Map([
- ['onScanResult', { callback: onScanResultHelper, listener: undefined }],
- ['onConnect', { callback: onConnectCallback, listener: undefined }],
- ['onDisconnect', { callback: onDisconnectCallback, listener: undefined }],
+ ['onScanResult', onScanResultHelper],
+ ['onConnect', onConnectCallback],
+ ['onDisconnect', onDisconnectCallback],
]);
- listeners.forEach((callbackObj: any, listenerName: string) => {
- callbackObj.listener = this.eventEmitter!.addListener(listenerName, callbackObj.callback);
- this.listeners.set(listenerName, callbackObj.listener);
+ listeners.forEach((callback: any, listenerName: string) => {
+ const listener = this.eventEmitter!.addListener(listenerName, callback);
+ this.listeners.set(listenerName, listener);
});
NativeModuleBle.requestLEScan(
@@ -188,12 +182,11 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
timeout: timeout,
}),
);
-
setTimeout(
() => {
- listeners.forEach((callbackObj: any, listenerName: string) => {
+ this.listeners.forEach((listener, listenerName) => {
+ listener?.remove();
this.listeners.set(listenerName, undefined);
- callbackObj.listener.remove();
});
Promise.resolve();
},
@@ -218,23 +211,23 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
['onDisconnect', undefined],
]);
NativeModuleBle.setSupportedEvents({ events: [...this.listeners.keys()] });
- return await NativeModuleBle.disconnect(options);
+ return NativeModuleBle.disconnect(options);
}
async read(options: ReadOptions & TimeoutOptions): Promise {
- return await NativeModuleBle.read(options);
+ return NativeModuleBle.read(options);
}
async write(options: WriteOptions & TimeoutOptions): Promise {
- return await NativeModuleBle.write(options);
+ return NativeModuleBle.write(options);
}
async signalize(options: SignalizeOptions): Promise {
- return await NativeModuleBle.signalize(options);
+ return NativeModuleBle.signalize(options);
}
async disengage(options: DisengageOptions): Promise {
- return await NativeModuleBle.disengage(options);
+ return NativeModuleBle.disengage(options);
}
async startNotifications(
@@ -243,15 +236,11 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
characteristic: string,
callback: (event: ReadResult) => void,
): Promise {
- if (this.eventEmitter === undefined) {
- console.error('unsupported platform');
- return;
- }
const key = `notification|${deviceId}|${service}|${characteristic}`.toLowerCase();
- const listener = this.eventEmitter.addListener(key, callback);
+ const listener = this.eventEmitter!.addListener(key, callback);
this.listeners.set(key, listener);
await NativeModuleBle.setSupportedEvents({ events: [...this.listeners.keys()] });
- return await NativeModuleBle.startNotifications(
+ return NativeModuleBle.startNotifications(
this.removeUndefinedField({
deviceId: deviceId,
service: service,
@@ -260,16 +249,24 @@ class AbrevvaBleModule implements AbrevvaBLEInterface {
);
}
- async stopNotifications(options: ReadOptions): Promise {
- const key =
- `notification|${options.deviceId}|${options.service}|${options.characteristic}`.toLowerCase();
- if (key in this.listeners) {
+ async stopNotifications(
+ deviceId: string,
+ service: string,
+ characteristic: string,
+ ): Promise {
+ const key = `notification|${deviceId}|${service}|${characteristic}`.toLowerCase();
+ if (this.listeners.get(key)) {
+ this.listeners.get(key)?.remove();
this.listeners.delete(key);
NativeModuleBle.setSupportedEvents({
events: [...this.listeners.keys()],
});
}
- return await NativeModuleBle.stopNotifications(options);
+ return NativeModuleBle.stopNotifications({
+ deviceId: deviceId,
+ service: service,
+ characteristic: characteristic,
+ });
}
}
@@ -313,7 +310,7 @@ class AbrevvaCryptoModule implements AbrevvaCryptoInterface {
}
encryptFile(sharedSecret: string, ptPath: string, ctPath: string) {
- return NativeModuleCrypto.encrypt({
+ return NativeModuleCrypto.encryptFile({
sharedSecret: sharedSecret,
ptPath: ptPath,
ctPath: ctPath,
@@ -348,3 +345,7 @@ class AbrevvaCryptoModule implements AbrevvaCryptoInterface {
export const AbrevvaBle = new AbrevvaBleModule();
export const AbrevvaCrypto = new AbrevvaCryptoModule();
export const AbrevvaNfc = new AbrevvaNfcModule();
+
+export function createAbrevvaBleInstance() {
+ return new AbrevvaBleModule();
+}
diff --git a/src/interfaces.tsx b/src/interfaces.tsx
index 5b2aec6..cec83ed 100644
--- a/src/interfaces.tsx
+++ b/src/interfaces.tsx
@@ -151,5 +151,5 @@ export interface AbrevvaBLEInterface {
characteristic: string,
callback: (event: ReadResult) => void,
): Promise;
- stopNotifications(options: ReadOptions): Promise;
+ stopNotifications(deviceId: string, service: string, characteristic: string): Promise;
}
diff --git a/src/setup.tsx b/src/setup.tsx
new file mode 100644
index 0000000..0a9702c
--- /dev/null
+++ b/src/setup.tsx
@@ -0,0 +1,57 @@
+jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => ({
+ __esModule: true,
+ default: class {
+ addListener = () => jest.fn();
+ removeListener = () => jest.fn();
+ removeAllListeners = () => jest.fn();
+ },
+}));
+
+jest.mock('react-native/Libraries/Utilities/Platform', () => ({
+ OS: 'ios',
+ select: jest.fn(() => null),
+}));
+
+jest.mock('react-native', () => {
+ const originalModule = jest.requireActual('react-native');
+ originalModule.NativeModules.AbrevvaNfc = {
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+ read: jest.fn(),
+ };
+ originalModule.NativeModules.AbrevvaBle = {
+ setSupportedEvents: jest.fn(),
+ initialize: jest.fn(),
+ isEnabled: jest.fn(),
+ isLocationEnabled: jest.fn(),
+ startEnabledNotifications: jest.fn(),
+ stopEnabledNotifications: jest.fn(),
+ openLocationSettings: jest.fn(),
+ openBluetoothSettings: jest.fn(),
+ openAppSettings: jest.fn(),
+ requestLEScan: jest.fn(),
+ stopLEScan: jest.fn(),
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+ read: jest.fn(),
+ write: jest.fn(),
+ signalize: jest.fn(),
+ disengage: jest.fn(),
+ startNotifications: jest.fn(),
+ stopNotifications: jest.fn(),
+ addListener: jest.fn(),
+ removeListeners: jest.fn(),
+ };
+ originalModule.NativeModules.AbrevvaCrypto = {
+ encrypt: jest.fn(),
+ decrypt: jest.fn(),
+ generateKeyPair: jest.fn(),
+ computeSharedSecret: jest.fn(),
+ encryptFile: jest.fn(),
+ decryptFile: jest.fn(),
+ decryptFileFromURL: jest.fn(),
+ random: jest.fn(),
+ derive: jest.fn(),
+ };
+ return originalModule;
+});
diff --git a/tsconfig.json b/tsconfig.json
index bda6782..8271f5c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
"compilerOptions": {
"rootDir": ".",
"paths": {
- "react-native-example-app": ["./src/index"]
+ "@evva-sfw/abrevva-react-native": ["./src/index"]
},
"allowUnreachableCode": false,
"allowUnusedLabels": false,
diff --git a/yarn.lock b/yarn.lock
index eeded97..96b255c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1938,11 +1938,11 @@ __metadata:
linkType: hard
"@evilmartians/lefthook@npm:^1.7.11":
- version: 1.7.11
- resolution: "@evilmartians/lefthook@npm:1.7.11"
+ version: 1.7.12
+ resolution: "@evilmartians/lefthook@npm:1.7.12"
bin:
lefthook: bin/index.js
- checksum: aedc85150bcdddf9b1a8cefe7883794e95b9441854393aeaba662c1cb396ca79ba5995fd225921565a0b7068a7b5ff13909e3f509d9ab317d19095909f6cd1ad
+ checksum: 2c2e19b298ca40dca6d87482e997bb559f4a481d39453dd8893b60b5e9cff8b8db89917eee38e9f8c482e91d26040ea1aa5a1da11cec6c93a6501a1c6cb8ce6a
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=ia32)
languageName: node
linkType: hard
@@ -2600,13 +2600,13 @@ __metadata:
linkType: hard
"@pnpm/npm-conf@npm:^2.1.0":
- version: 2.3.0
- resolution: "@pnpm/npm-conf@npm:2.3.0"
+ version: 2.3.1
+ resolution: "@pnpm/npm-conf@npm:2.3.1"
dependencies:
"@pnpm/config.env-replace": ^1.1.0
"@pnpm/network.ca-file": ^1.0.1
config-chain: ^1.1.11
- checksum: df071050bad2c7f750a349330428d5e681047fbe65ce043d30fa3c75a6cb02e16eb08c56d310d1bc6bb6c5606d70d95bc5e30529424c35c78505449eb9bd8e2c
+ checksum: 9e1e1ce5faa64719e866b02d10e28d727d809365eb3692ccfdc420ab6d2073b93abe403994691868f265e34a5601a8eee18ffff6562b27124d971418ba6bb815
languageName: node
linkType: hard
@@ -3340,20 +3340,20 @@ __metadata:
linkType: hard
"@types/node@npm:*":
- version: 22.1.0
- resolution: "@types/node@npm:22.1.0"
+ version: 22.2.0
+ resolution: "@types/node@npm:22.2.0"
dependencies:
undici-types: ~6.13.0
- checksum: 3544c35da06009790a2e07742a7dfa0ac0f0d64ec47d9e6d3edf0ff6dcfc1a7cc2efdc5e524e80f8ed80aa37154513b2c1c724f95146ff89fc5aefb8e33575f2
+ checksum: 63724799337bfb57719a1992690e738341d824e1744a2ac52c5278a008fbfadf99765519c19858feb80418cc7da0d5c8bdf7ea4d82973869b3882bd602c48ade
languageName: node
linkType: hard
"@types/node@npm:^18.0.0":
- version: 18.19.43
- resolution: "@types/node@npm:18.19.43"
+ version: 18.19.44
+ resolution: "@types/node@npm:18.19.44"
dependencies:
undici-types: ~5.26.4
- checksum: 5eb9045aae6da86e8ad297381f93d29d2e7fcd4ed0c53670d9dff1e7b714920f8bbe5ee456289c19fc69c510ac197bdbacc7a785eaeba0afb9cb5d634a64bcd3
+ checksum: 008f89b04afc9fdb4cd5ea71072ca64a08ef0453cbfc012863991959aa3ce2cf99c1e73cbcb5e0e67b37a81f88673e97ac180f56eec396ce1b68d5881aac2ce4
languageName: node
linkType: hard
@@ -3636,6 +3636,30 @@ __metadata:
languageName: node
linkType: hard
+"abrevva-react-native-example@workspace:example":
+ version: 0.0.0-use.local
+ resolution: "abrevva-react-native-example@workspace:example"
+ dependencies:
+ "@babel/core": ^7.20.0
+ "@babel/preset-env": ^7.20.0
+ "@babel/runtime": ^7.20.0
+ "@react-native/babel-preset": 0.74.85
+ "@react-native/metro-config": 0.74.85
+ "@react-native/typescript-config": 0.74.85
+ "@react-navigation/native": ^6.1.18
+ "@react-navigation/native-stack": ^6.10.1
+ "@scure/base": ^1.1.7
+ babel-plugin-module-resolver: ^5.0.0
+ binary-parser-encoder: ^1.5.3
+ react: 18.2.0
+ react-native: 0.74.3
+ react-native-fs: ^2.20.0
+ react-native-safe-area-context: ^4.10.8
+ react-native-screens: ^3.32.0
+ util: ^0.12.5
+ languageName: unknown
+ linkType: soft
+
"accepts@npm:^1.3.7, accepts@npm:~1.3.5, accepts@npm:~1.3.7":
version: 1.3.8
resolution: "accepts@npm:1.3.8"
@@ -5480,9 +5504,9 @@ __metadata:
linkType: hard
"electron-to-chromium@npm:^1.5.4":
- version: 1.5.5
- resolution: "electron-to-chromium@npm:1.5.5"
- checksum: fcdd2797ece1ece6764b88b5fc36cfc6a571e08b832c6777d8bbefa19cae22a36614411aacc5687d9fea7e1db86469f53c3952ca2579c5fe705dea7ed270d8cc
+ version: 1.5.6
+ resolution: "electron-to-chromium@npm:1.5.6"
+ checksum: 09ca45c781e3e3ef87de87fb74019228f41e1a4abd2e703319aa7d942866815f3df89cc4bf61af81a4cac81271992d4f59a5eca7a093c07322ae0608bf98a427
languageName: node
linkType: hard
@@ -7224,9 +7248,9 @@ __metadata:
linkType: hard
"ignore@npm:^5.0.5, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1":
- version: 5.3.1
- resolution: "ignore@npm:5.3.1"
- checksum: 71d7bb4c1dbe020f915fd881108cbe85a0db3d636a0ea3ba911393c53946711d13a9b1143c7e70db06d571a5822c0a324a6bcde5c9904e7ca5047f01f1bf8cd3
+ version: 5.3.2
+ resolution: "ignore@npm:5.3.2"
+ checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be
languageName: node
linkType: hard
@@ -10821,30 +10845,6 @@ __metadata:
languageName: node
linkType: hard
-"react-native-example-app-example@workspace:example":
- version: 0.0.0-use.local
- resolution: "react-native-example-app-example@workspace:example"
- dependencies:
- "@babel/core": ^7.20.0
- "@babel/preset-env": ^7.20.0
- "@babel/runtime": ^7.20.0
- "@react-native/babel-preset": 0.74.85
- "@react-native/metro-config": 0.74.85
- "@react-native/typescript-config": 0.74.85
- "@react-navigation/native": ^6.1.18
- "@react-navigation/native-stack": ^6.10.1
- "@scure/base": ^1.1.7
- babel-plugin-module-resolver: ^5.0.0
- binary-parser-encoder: ^1.5.3
- react: 18.2.0
- react-native: 0.74.3
- react-native-fs: ^2.20.0
- react-native-safe-area-context: ^4.10.8
- react-native-screens: ^3.32.0
- util: ^0.12.5
- languageName: unknown
- linkType: soft
-
"react-native-fs@npm:^2.20.0":
version: 2.20.0
resolution: "react-native-fs@npm:2.20.0"
@@ -12624,11 +12624,11 @@ __metadata:
linkType: hard
"uglify-js@npm:^3.1.4":
- version: 3.19.1
- resolution: "uglify-js@npm:3.19.1"
+ version: 3.19.2
+ resolution: "uglify-js@npm:3.19.2"
bin:
uglifyjs: bin/uglifyjs
- checksum: c71e455b0adcc75631effbcc8fa868e3e906c97b73be034ccdb1373babfa30c3378356febec6bd9abe756dad80ba156af9654b9540244ab9950b56d2543cd54b
+ checksum: 2236220638223f72340d770daa46704a6f54bcd3022e04510a55bb693a40c32e38a9a439333703f16c9880226cc9952c0dddfe67e7b870c287d915b54757ab51
languageName: node
linkType: hard