Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Android implementation for Cordova #210

Merged
merged 25 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3f95c80
Improve testapp-cordova scripts
TomasKypta Sep 6, 2024
5133018
Fix cordova app author
TomasKypta Sep 6, 2024
6a113d1
Update .gitignore in testapp-cordova
TomasKypta Sep 6, 2024
0658cde
Refactor android code to separate React-specific code
TomasKypta Sep 18, 2024
c96ebac
Update test app scripts
TomasKypta Sep 18, 2024
5f94613
Fix Android warnings
TomasKypta Sep 18, 2024
1dc7d0d
Move manifest package to namespace
TomasKypta Sep 18, 2024
f1d81b2
Add basis of cordova implementation
TomasKypta Sep 25, 2024
6acf2bd
Updated plugin xml
kober32 Sep 25, 2024
5f748a5
Resolve most of compilation errors
TomasKypta Sep 25, 2024
e9d2a5c
Fixed tests
kober32 Sep 26, 2024
ca351f9
Fix compile errors
TomasKypta Sep 26, 2024
efab512
Fix CORS issue
TomasKypta Sep 27, 2024
1376f24
Fix Android cleartext error
TomasKypta Sep 30, 2024
df4719c
Fix some Cordova Android bugs
TomasKypta Oct 1, 2024
4e21b38
Fix Cordova Android bugs and tests
TomasKypta Oct 1, 2024
71e2810
Clean Cordova map and array implementations
TomasKypta Oct 2, 2024
e78a5d5
Improve Cordova Android scripts
TomasKypta Oct 2, 2024
9dd9923
Refactor Android's Dynamic type implementation
TomasKypta Oct 2, 2024
e62fc1b
Remove dev logs from Cordova Android
TomasKypta Oct 2, 2024
5b8605a
Fix Android warnings in Cordova
TomasKypta Oct 2, 2024
319f5d2
Merge branch 'cordova' into cordova-android-01
TomasKypta Oct 2, 2024
a44915b
Fix Cordova iOS
TomasKypta Oct 3, 2024
8e984c4
Improve build scripts
TomasKypta Oct 3, 2024
d031491
Add handling of DEBUG for Cordova
TomasKypta Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
ext {
//
// Defaults for the standalone build
//
PowerAuthSdkRN_kotlinVersion = "1.7.0"

PowerAuthSdkRN_kotlinVersion = "1.9.22"
PowerAuthSdkRN_minSdkVersion = 21
PowerAuthSdkRN_targetSdkVersion = 33
PowerAuthSdkRN_compileSdkVersion = 33
Expand All @@ -26,6 +26,7 @@ def isNewArchitectureEnabled() {
}

apply plugin: "com.android.library"
apply plugin: "org.jetbrains.kotlin.android"

if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
Expand All @@ -40,6 +41,7 @@ def getExtOrIntegerDefault(name) {
}

android {
namespace = "com.wultra.android.powerauth.reactnative"
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")

defaultConfig {
Expand Down Expand Up @@ -69,6 +71,7 @@ repositories {
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
api "com.wultra.android.powerauth:powerauth-sdk:1.7.9"

if (project == rootProject) {
Expand Down
4 changes: 2 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wultra.android.powerauth.reactnative">
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.wultra.android.powerauth.bridge

import android.content.Context

// point to React bridge
typealias Arguments = com.facebook.react.bridge.Arguments
typealias Dynamic = com.facebook.react.bridge.Dynamic
typealias Promise = com.facebook.react.bridge.Promise
typealias ReactMethod = com.facebook.react.bridge.ReactMethod
typealias ReadableArray = com.facebook.react.bridge.ReadableArray
typealias ReadableMap = com.facebook.react.bridge.ReadableMap
typealias ReadableType = com.facebook.react.bridge.ReadableType
typealias WritableArray = com.facebook.react.bridge.WritableArray
typealias WritableMap = com.facebook.react.bridge.WritableMap



// bridge to concrete platform implementation
typealias BuildConfig = com.wultra.android.powerauth.reactnative.BuildConfig
typealias PwBuildConfig = com.wultra.android.powerauth.bridge.ReactPwBuildConfig

object ReactPwBuildConfig {
fun isDebuggable(context: Context) = BuildConfig.DEBUG
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.wultra.android.powerauth.js

import android.app.Activity

/**
* Module aware of the activity.
*/
interface ActivityAwareModule {

fun getCurrentActivity(): Activity
}

/**
* Provider of activity.
*/
interface ActivityProvider {
fun getActivity(): Activity
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wultra.android.powerauth.js

/**
* Module base.
*/
public interface BaseJavaJsModule {

fun getName(): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.wultra.android.powerauth.reactnative;
package com.wultra.android.powerauth.js;

class Constants {
/**
Expand Down
110 changes: 110 additions & 0 deletions android/src/main/java/com/wultra/android/powerauth/js/DataFormat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wultra.android.powerauth.js

import android.util.Base64
import com.wultra.android.powerauth.js.WrapperException
import java.nio.charset.StandardCharsets

/**
* Defines data format used for encode bytes into the string.
*/
enum class DataFormat {
/**
* Application provides data in form of UTF-8 encoded string.
*/
UTF8,

/**
* Application provides data in form of Base64 encoded string.
*/
BASE64;

/**
* Decode bytes from application provided string with using this data format.
* @param value String with encoded bytes.
* @return Decoded bytes.
* @throws WrapperException In case of failure.
*/
@Throws(WrapperException::class)
fun decodeBytes(value: String?): ByteArray? {
val result: ByteArray?
if (value != null) {
result = if (this == UTF8) {
value.toByteArray(StandardCharsets.UTF_8)
} else {
try {
Base64.decode(value, Base64.NO_WRAP)
} catch (e: IllegalArgumentException) {
throw WrapperException(
Errors.EC_WRONG_PARAMETER,
"Failed to decode Base64 encoded data.",
e
)
}
}
} else {
result = ByteArray(0)
}
return result
}

/**
* Encode bytes into this data format.
* @param value Bytes to encode.
* @return Encoded bytes.
* @throws WrapperException In case of failure.
*/
@Throws(WrapperException::class)
fun encodeBytes(value: ByteArray?): String {
if (value == null || value.size == 0) {
return ""
}
return if (this == UTF8) {
try {
String(value, StandardCharsets.UTF_8)
} catch (t: Throwable) {
throw WrapperException(
Errors.EC_WRONG_PARAMETER,
"Failed to create string from UTF-8 encoded data",
t
)
}
} else {
Base64.encodeToString(value, Base64.NO_WRAP)
}
}

companion object {
/**
* Convert format string into this enumeration.
* @param format Specified data format. If `null` then `UTF8` is returned.
* @return Enumeration with data format.
* @throws WrapperException In case of uknown format is specified.
*/
@Throws(WrapperException::class)
fun fromString(format: String?): DataFormat {
if (format == null) {
return UTF8
} else if ("UTF8" == format) {
return UTF8
} else if ("BASE64" == format) {
return BASE64
}
throw WrapperException(Errors.EC_WRONG_PARAMETER, "Invalid data format specified")
}
}
}
173 changes: 173 additions & 0 deletions android/src/main/java/com/wultra/android/powerauth/js/Errors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2022 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wultra.android.powerauth.js

import android.annotation.SuppressLint
import com.wultra.android.powerauth.bridge.Arguments
import com.wultra.android.powerauth.bridge.Promise
import com.wultra.android.powerauth.bridge.WritableMap
import io.getlime.security.powerauth.exception.PowerAuthErrorCodes
import io.getlime.security.powerauth.exception.PowerAuthErrorException
import io.getlime.security.powerauth.networking.exceptions.ErrorResponseApiException
import io.getlime.security.powerauth.networking.exceptions.FailedApiException
import java.io.IOException

@Suppress("MemberVisibilityCanBePrivate")
object Errors {
// RN specific
const val EC_REACT_NATIVE_ERROR: String = "REACT_NATIVE_ERROR"
const val EC_AUTHENTICATION_ERROR: String = "AUTHENTICATION_ERROR"
const val EC_RESPONSE_ERROR: String = "RESPONSE_ERROR"
const val EC_INSTANCE_NOT_CONFIGURED: String = "INSTANCE_NOT_CONFIGURED"
const val EC_INVALID_CHARACTER: String = "INVALID_CHARACTER"
const val EC_INVALID_RECOVERY_CODE: String = "INVALID_RECOVERY_CODE"
const val EC_CANNOT_GENERATE_TOKEN: String = "CANNOT_GENERATE_TOKEN"
const val EC_LOCAL_TOKEN_NOT_AVAILABLE: String = "LOCAL_TOKEN_NOT_AVAILABLE"
const val EC_BIOMETRY_FAILED: String = "BIOMETRY_FAILED"
const val EC_INVALID_ACTIVATION_OBJECT: String = "INVALID_ACTIVATION_OBJECT"
const val EC_INVALID_NATIVE_OBJECT: String = "INVALID_NATIVE_OBJECT"

// Translated PowerAuthErrorCodes
const val EC_SUCCEED: String = "SUCCEED"
const val EC_NETWORK_ERROR: String = "NETWORK_ERROR"
const val EC_SIGNATURE_ERROR: String = "SIGNATURE_ERROR"
const val EC_INVALID_ACTIVATION_STATE: String = "INVALID_ACTIVATION_STATE"
const val EC_INVALID_ACTIVATION_DATA: String = "INVALID_ACTIVATION_DATA"
const val EC_MISSING_ACTIVATION: String = "MISSING_ACTIVATION"
const val EC_PENDING_ACTIVATION: String = "PENDING_ACTIVATION"
const val EC_BIOMETRY_CANCEL: String = "BIOMETRY_CANCEL"
const val EC_OPERATION_CANCELED: String = "OPERATION_CANCELED"
const val EC_INVALID_ACTIVATION_CODE: String = "INVALID_ACTIVATION_CODE"
const val EC_INVALID_TOKEN: String = "INVALID_TOKEN"
const val EC_INVALID_ENCRYPTOR: String = "INVALID_ENCRYPTOR"
const val EC_ENCRYPTION_ERROR: String = "ENCRYPTION_ERROR"
const val EC_WRONG_PARAMETER: String = "WRONG_PARAMETER"
const val EC_PROTOCOL_UPGRADE: String = "PROTOCOL_UPGRADE"
const val EC_PENDING_PROTOCOL_UPGRADE: String = "PENDING_PROTOCOL_UPGRADE"
const val EC_BIOMETRY_NOT_SUPPORTED: String = "BIOMETRY_NOT_SUPPORTED"
const val EC_BIOMETRY_NOT_AVAILABLE: String = "BIOMETRY_NOT_AVAILABLE"
const val EC_BIOMETRY_NOT_RECOGNIZED: String = "BIOMETRY_NOT_RECOGNIZED"
const val EC_BIOMETRY_NOT_CONFIGURED: String = "BIOMETRY_NOT_CONFIGURED"
const val EC_BIOMETRY_NOT_ENROLLED: String = "BIOMETRY_NOT_ENROLLED"
const val EC_INSUFFICIENT_KEYCHAIN_PROTECTION: String = "INSUFFICIENT_KEYCHAIN_PROTECTION"
const val EC_BIOMETRY_LOCKOUT: String = "BIOMETRY_LOCKOUT"
const val EC_UNKNOWN_ERROR: String = "UNKNOWN_ERROR"

/**
* Translate `PowerAuthErrorCodes` error constant into string representation.
* @param error Error code to translate.
* @return String representation of given error code.
*/
@SuppressLint("DefaultLocale")
fun getErrorCodeFromError(@PowerAuthErrorCodes error: Int): String {
return when (error) {
PowerAuthErrorCodes.SUCCEED -> EC_SUCCEED
PowerAuthErrorCodes.NETWORK_ERROR -> EC_NETWORK_ERROR
PowerAuthErrorCodes.SIGNATURE_ERROR -> EC_SIGNATURE_ERROR
PowerAuthErrorCodes.INVALID_ACTIVATION_STATE -> EC_INVALID_ACTIVATION_STATE
PowerAuthErrorCodes.INVALID_ACTIVATION_DATA -> EC_INVALID_ACTIVATION_DATA
PowerAuthErrorCodes.MISSING_ACTIVATION -> EC_MISSING_ACTIVATION
PowerAuthErrorCodes.PENDING_ACTIVATION -> EC_PENDING_ACTIVATION
PowerAuthErrorCodes.BIOMETRY_CANCEL -> EC_BIOMETRY_CANCEL
PowerAuthErrorCodes.OPERATION_CANCELED -> EC_OPERATION_CANCELED
PowerAuthErrorCodes.INVALID_ACTIVATION_CODE -> EC_INVALID_ACTIVATION_CODE
PowerAuthErrorCodes.INVALID_TOKEN -> EC_INVALID_TOKEN
PowerAuthErrorCodes.ENCRYPTION_ERROR -> EC_ENCRYPTION_ERROR
PowerAuthErrorCodes.WRONG_PARAMETER -> EC_WRONG_PARAMETER
PowerAuthErrorCodes.PROTOCOL_UPGRADE -> EC_PROTOCOL_UPGRADE
PowerAuthErrorCodes.PENDING_PROTOCOL_UPGRADE -> EC_PENDING_PROTOCOL_UPGRADE
PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED -> EC_BIOMETRY_NOT_SUPPORTED
PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE -> EC_BIOMETRY_NOT_AVAILABLE
PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED -> EC_BIOMETRY_NOT_RECOGNIZED
PowerAuthErrorCodes.INSUFFICIENT_KEYCHAIN_PROTECTION -> EC_INSUFFICIENT_KEYCHAIN_PROTECTION
PowerAuthErrorCodes.BIOMETRY_LOCKOUT -> EC_BIOMETRY_LOCKOUT
else -> String.format("UNKNOWN_%d", error)
}
}

/**
* Reject promise with given error. The provided error is automatically translated to
* a proper error code returned to RN, depending on the type of exception.
*
* @param promise Promise to reject.
* @param t Error to report.
*/
fun rejectPromise(promise: Promise, t: Throwable) {
var code = EC_REACT_NATIVE_ERROR // fallback code
var message = t.message
var userInfo: WritableMap? = null

if (t is WrapperException) {
// Exceptions produced in this module
code = t.errorCode
} else if (t is PowerAuthErrorException) {
// Standard PowerAuthErrorException, containing enumeration with error code.
code = getErrorCodeFromError(t.powerAuthErrorCode)
} else if (t is FailedApiException) {
// FailedApiException or more specialized ErrorResponseApiException
val failedApiException: FailedApiException = t
val httpStatusCode: Int = failedApiException.responseCode
if (httpStatusCode == 401) {
code = EC_AUTHENTICATION_ERROR
message = "Unauthorized"
} else {
code = EC_RESPONSE_ERROR
}
//
userInfo = Arguments.createMap()
userInfo.putInt("httpStatusCode", httpStatusCode)
userInfo.putString("responseBody", failedApiException.responseBody)
if (t is ErrorResponseApiException) {
// ErrorResponseApiException is more specialized version of FailedApiException, containing
// an additional data.
val errorResponseApiException: ErrorResponseApiException = t
val currentRecoveryPukIndex: Int =
errorResponseApiException.getCurrentRecoveryPukIndex()
if (currentRecoveryPukIndex > 0) {
userInfo.putInt("currentRecoveryPukIndex", currentRecoveryPukIndex)
}
userInfo.putString(
"serverResponseCode",
errorResponseApiException.errorResponse.code
)
userInfo.putString(
"serverResponseMessage",
errorResponseApiException.errorResponse.message
)
}
} else {
userInfo = Arguments.createMap()
// When we don't process the exception, at least pass the original name of the exception for logging and troubleshooting purposes.
userInfo.putString("nativeExceptionTypeName", t.javaClass.simpleName)

if (t is IOException) {
// Consider IOExceptions a network error
// Usually its something like UnknownHostException, ConnectException or SocketException.
code = EC_NETWORK_ERROR
}
}

if (message != null && userInfo != null) {
promise.reject(code, message, t, userInfo)
} else if (message != null) {
promise.reject(code, message, t)
} else if (userInfo != null) {
promise.reject(code, t, userInfo)
} else {
promise.reject(code, t)
}
}
}
Loading
Loading