feat(react-native): expo support (#2850)

* change(react-native): android version jump

* change(react-native): updates to support expo

* change(react-native): swipe event fix

* change(react-native): version jump

* change(react-native): include plugin file and version jump
This commit is contained in:
Shekar Siri 2024-12-11 09:41:16 +01:00 committed by GitHub
parent e74effe24d
commit 9a37ba0739
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 217 deletions

View file

@ -91,7 +91,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation("com.facebook.react:react-native:0.20.1")
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
implementation("com.github.openreplay:android-tracker:v1.1.2")
implementation("com.github.openreplay:android-tracker:v1.1.3")
}
//allprojects {

View file

@ -10,18 +10,10 @@ import com.openreplay.tracker.models.OROptions
class ReactNativeModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
// private val context = reactContext.acti
override fun getName(): String {
return NAME
}
// Example method
// See https://reactnative.dev/docs/native-modules-android
@ReactMethod
fun multiply(a: Double, b: Double, promise: Promise) {
promise.resolve(a * b * 2)
}
companion object {
const val NAME = "ORTrackerConnector"
}
@ -33,14 +25,13 @@ class ReactNativeModule(reactContext: ReactApplicationContext) :
val logs: Boolean = true,
val screen: Boolean = true,
val debugLogs: Boolean = false,
val wifiOnly: Boolean = true // assuming you want this as well
val wifiOnly: Boolean = true
)
private fun getBooleanOrDefault(map: ReadableMap, key: String, default: Boolean): Boolean {
return if (map.hasKey(key)) map.getBoolean(key) else default
}
// optionsMap: ReadableMap?,
@ReactMethod
fun startSession(
projectKey: String,
@ -97,8 +88,8 @@ class ReactNativeModule(reactContext: ReactApplicationContext) :
@ReactMethod
fun getSessionID(promise: Promise) {
try {
val sessionId = OpenReplay.getSessionID() ?: ""
promise.resolve(sessionId) // Resolve the promise with the session ID
val sessionId = OpenReplay.getSessionID()
promise.resolve(sessionId)
} catch (e: Exception) {
promise.reject("GET_SESSION_ID_ERROR", "Failed to retrieve session ID", e)
}
@ -111,8 +102,9 @@ class ReactNativeModule(reactContext: ReactApplicationContext) :
requestJSON: String,
responseJSON: String,
status: Int,
duration: ULong
duration: Double
) {
OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, duration)
val durationULong = duration.toLong().toULong()
OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, durationULong)
}
}

View file

@ -1,13 +1,13 @@
package com.openreplay.reactnative
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PointF
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.openreplay.tracker.listeners.Analytics
@ -15,151 +15,16 @@ import com.openreplay.tracker.listeners.SwipeDirection
import kotlin.math.abs
import kotlin.math.sqrt
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.GestureDetector
import com.facebook.react.ReactRootView
//class RnTrackerTouchManager : ViewGroupManager<TouchableFrameLayout>() {
// override fun getName(): String = "RnTrackerTouchView"
//
// override fun createViewInstance(reactContext: ThemedReactContext): TouchableFrameLayout {
// return TouchableFrameLayout(reactContext)
// }
//}
//
//class TouchableFrameLayout(context: Context) : FrameLayout(context) {
// private var gestureDetector: GestureDetector
// private var handler = Handler(Looper.getMainLooper())
// private var isScrolling = false
// private var lastX: Float = 0f
// private var lastY: Float = 0f
// private var swipeDirection: SwipeDirection = SwipeDirection.UNDEFINED
//
// init {
// gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
// override fun onSingleTapUp(e: MotionEvent): Boolean {
// Analytics.sendClick(e)
// return true
// }
//
// override fun onDown(e: MotionEvent): Boolean = true
//
// override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
// if (!isScrolling) {
// isScrolling = true
// }
//
// swipeDirection = SwipeDirection.fromDistances(distanceX, distanceY)
// lastX = e2.x
// lastY = e2.y
//
// handler.removeCallbacksAndMessages(null)
// handler.postDelayed({
// if (isScrolling) {
// isScrolling = false
// Analytics.sendSwipe(swipeDirection, lastX, lastY)
// }
// }, 200)
// return true
// }
// })
//
// setOnTouchListener { _, event ->
// Log.d("TouchEvent", "Event: ${event.actionMasked}, X: ${event.x}, Y: ${event.y}")
// gestureDetector.onTouchEvent(event)
// this.performClick()
// }
// }
//}
class RnTrackerTouchManager : ViewGroupManager<FrameLayout>() {
override fun getName(): String = "RnTrackerTouchView"
override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout {
return ReactRootView(reactContext).apply {
// layoutParams = FrameLayout.LayoutParams(
// FrameLayout.LayoutParams.MATCH_PARENT,
// FrameLayout.LayoutParams.MATCH_PARENT
// )
// isClickable = true
// val touchStart = PointF()
// setOnTouchListener { view, event ->
// when (event.action) {
// MotionEvent.ACTION_DOWN -> {
// touchStart.set(event.x, event.y)
// true
// }
//
// MotionEvent.ACTION_UP -> {
// val deltaX = event.x - touchStart.x
// val deltaY = event.y - touchStart.y
// val distance = sqrt(deltaX * deltaX + deltaY * deltaY)
//
// if (distance > 10) {
// val direction = if (abs(deltaX) > abs(deltaY)) {
// if (deltaX > 0) "RIGHT" else "LEFT"
// } else {
// if (deltaY > 0) "DOWN" else "UP"
// }
// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y)
// } else {
// Analytics.sendClick(event)
// view.performClick() // Perform click for accessibility
// }
// true
// }
//
// else -> false
// }
// }
}
return RnTrackerRootLayout(reactContext)
}
override fun addView(parent: FrameLayout, child: View, index: Int) {
child.isClickable = true
child.isFocusable = true
// child.layoutParams = FrameLayout.LayoutParams(
// FrameLayout.LayoutParams.MATCH_PARENT,
// FrameLayout.LayoutParams.MATCH_PARENT
// )
val touchStart = PointF()
child.setOnTouchListener(
View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
view.performClick()
Analytics.sendClick(event)
true
}
MotionEvent.ACTION_UP -> {
val deltaX = event.x - touchStart.x
val deltaY = event.y - touchStart.y
val distance = sqrt(deltaX * deltaX + deltaY * deltaY)
if (distance > 10) {
val direction = if (abs(deltaX) > abs(deltaY)) {
if (deltaX > 0) "RIGHT" else "LEFT"
} else {
if (deltaY > 0) "DOWN" else "UP"
}
Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y)
} else {
Analytics.sendClick(event)
view.performClick() // Perform click for accessibility
}
true
}
else -> false
}
}
)
parent.addView(child)
parent.addView(child, index)
}
override fun getChildCount(parent: FrameLayout): Int = parent.childCount
@ -175,63 +40,102 @@ class RnTrackerTouchManager : ViewGroupManager<FrameLayout>() {
}
}
//class RnTrackerTouchManager : ViewGroupManager<FrameLayout>() {
// override fun getName(): String = "RnTrackerTouchView"
//
// override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout {
// return FrameLayout(reactContext).apply {
// layoutParams = FrameLayout.LayoutParams(
// FrameLayout.LayoutParams.MATCH_PARENT,
// FrameLayout.LayoutParams.MATCH_PARENT
// )
// isClickable = true
// val touchStart = PointF()
// setOnTouchListener { view, event ->
// when (event.action) {
// MotionEvent.ACTION_DOWN -> {
// touchStart.set(event.x, event.y)
// view.performClick()
// }
//
// MotionEvent.ACTION_UP -> {
// val deltaX = event.x - touchStart.x
// val deltaY = event.y - touchStart.y
// val distance = sqrt(deltaX * deltaX + deltaY * deltaY)
//
// if (distance > 10) {
// val direction = if (abs(deltaX) > abs(deltaY)) {
// if (deltaX > 0) "RIGHT" else "LEFT"
// } else {
// if (deltaY > 0) "DOWN" else "UP"
// }
// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y)
// view.performClick()
// } else {
// Analytics.sendClick(event)
// view.performClick()
// }
// true
// }
//
// else -> false
// }
// }
// }
// }
//
// override fun addView(parent: FrameLayout, child: View, index: Int) {
// parent.addView(child, index)
// }
//
// override fun getChildCount(parent: FrameLayout): Int = parent.childCount
//
// override fun getChildAt(parent: FrameLayout, index: Int): View = parent.getChildAt(index)
//
// override fun removeViewAt(parent: FrameLayout, index: Int) {
// parent.removeViewAt(index)
// }
//
// override fun removeAllViews(parent: FrameLayout) {
// parent.removeAllViews()
// }
//}
class RnTrackerRootLayout(context: Context) : FrameLayout(context) {
private val touchStart = PointF()
private val gestureDetector: GestureDetector
private var currentTappedView: View? = null
// Variables to track total movement
private var totalDeltaX: Float = 0f
private var totalDeltaY: Float = 0f
init {
gestureDetector = GestureDetector(context, GestureListener())
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// Pass all touch events to the GestureDetector
gestureDetector.onTouchEvent(ev)
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
// Record the starting point for potential swipe
touchStart.x = ev.x
touchStart.y = ev.y
// Reset total movement
totalDeltaX = 0f
totalDeltaY = 0f
// Find and store the view that was touched
currentTappedView = findViewAt(this, ev.x.toInt(), ev.y.toInt())
// Log.d(
// "RnTrackerRootLayout",
// "ACTION_DOWN at global: (${ev.rawX}, ${ev.rawY}) on view: $currentTappedView"
// )
}
MotionEvent.ACTION_MOVE -> {
// Accumulate movement
val deltaX = ev.x - touchStart.x
val deltaY = ev.y - touchStart.y
totalDeltaX += deltaX
totalDeltaY += deltaY
// Update touchStart for the next move event
touchStart.x = ev.x
touchStart.y = ev.y
// Log.d("RnTrackerRootLayout", "Accumulated movement - X: $totalDeltaX, Y: $totalDeltaY")
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// Determine if the accumulated movement qualifies as a swipe
val distance = sqrt(totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY)
if (distance > SWIPE_DISTANCE_THRESHOLD) {
val direction = if (abs(totalDeltaX) > abs(totalDeltaY)) {
if (totalDeltaX > 0) "RIGHT" else "LEFT"
} else {
if (totalDeltaY > 0) "DOWN" else "UP"
}
Log.d("RnTrackerRootLayout", "Swipe detected: $direction")
Analytics.sendSwipe(SwipeDirection.valueOf(direction), ev.rawX, ev.rawY)
}
}
}
// Ensure normal event propagation
return super.dispatchTouchEvent(ev)
}
companion object {
private const val SWIPE_DISTANCE_THRESHOLD = 100f // Adjust as needed
}
private fun findViewAt(parent: ViewGroup, x: Int, y: Int): View? {
for (i in parent.childCount - 1 downTo 0) {
val child = parent.getChildAt(i)
if (isPointInsideView(x, y, child)) {
if (child is ViewGroup) {
val childX = x - child.left
val childY = y - child.top
val result = findViewAt(child, childX, childY)
return result ?: child
} else {
return child
}
}
}
return null
}
private fun isPointInsideView(x: Int, y: Int, view: View): Boolean {
return x >= view.left && x <= view.right && y >= view.top && y <= view.bottom
}
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
Log.d("GestureListener", "Single tap detected at: (${e.rawX}, ${e.rawY})")
val label = currentTappedView?.contentDescription?.toString() ?: "Button"
Analytics.sendClick(e, label)
currentTappedView?.performClick()
return super.onSingleTapUp(e)
}
}
}

View file

@ -0,0 +1,22 @@
const { withMainApplication } = require('@expo/config-plugins');
function addPackageToMainApplication(src) {
console.log('Adding OpenReplay package to MainApplication.java', src);
// Insert `packages.add(new ReactNativePackage());` before return packages;
if (src.includes('packages.add(new ReactNativePackage())')) {
return src;
}
return src.replace(
'return packages;',
`packages.add(new com.openreplay.reactnative.ReactNativePackage());\n return packages;`
);
}
module.exports = function configPlugin(config) {
return withMainApplication(config, (config) => {
if (config.modResults.contents) {
config.modResults.contents = addPackageToMainApplication(config.modResults.contents);
}
return config;
});
};

View file

@ -1,6 +1,6 @@
{
"name": "@openreplay/react-native",
"version": "0.6.6",
"version": "0.6.10",
"description": "Openreplay React-native connector for iOS applications",
"main": "lib/commonjs/index",
"module": "lib/module/index",
@ -13,6 +13,7 @@
"android",
"ios",
"cpp",
"app.plugin.js",
"*.podspec",
"!ios/build",
"!android/build",
@ -156,5 +157,8 @@
}
]
]
},
"dependencies": {
"@expo/config-plugins": "^9.0.12"
}
}