diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index 1dc4bfd5585544..4947c5124f0916 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -4,9 +4,9 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; +import org.wordpress.mobile.WPAndroidGlue.GutenbergJsException; import org.wordpress.mobile.WPAndroidGlue.MediaOption; import org.wordpress.mobile.WPAndroidGlue.RequestExecutor; @@ -14,7 +14,6 @@ import java.util.List; public interface GutenbergBridgeJS2Parent extends RequestExecutor { - void responseHtml(String title, String html, boolean changed, ReadableMap contentInfo); void editorDidMount(ReadableArray unsupportedBlockNames); @@ -65,6 +64,10 @@ interface ConnectionStatusCallback { void onRequestConnectionStatus(boolean isConnected); } + interface LogExceptionCallback { + void onLogException(boolean success); + } + // Ref: https://github.com/facebook/react-native/blob/HEAD/Libraries/polyfills/console.js#L376 enum LogLevel { TRACE(0), @@ -178,4 +181,6 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void toggleRedoButton(boolean isDisabled); void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback); + + void logException(GutenbergJsException exception, LogExceptionCallback logExceptionCallback); } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index f51edbff9bba1b..4f7066a5bd47d0 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -28,11 +28,13 @@ import com.facebook.react.modules.core.DeviceEventManagerModule; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ConnectionStatusCallback; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.LogExceptionCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaType; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.OtherMediaOptionsReceivedCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FocalPointPickerTooltipShownCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.BlockTypeImpressionsCallback; import org.wordpress.mobile.WPAndroidGlue.DeferredEventEmitter; +import org.wordpress.mobile.WPAndroidGlue.GutenbergJsException; import org.wordpress.mobile.WPAndroidGlue.MediaOption; import java.io.Serializable; @@ -593,4 +595,19 @@ public void hideAndroidSoftKeyboard() { } } } + + @ReactMethod + public void logException(final ReadableMap rawException, final Callback jsCallback) { + GutenbergJsException exception = GutenbergJsException.fromReadableMap(rawException); + LogExceptionCallback logExceptionCallback = onLogExceptionCallback(jsCallback); + mGutenbergBridgeJS2Parent.logException(exception, logExceptionCallback); + } + + private LogExceptionCallback onLogExceptionCallback(final Callback jsCallback) { + return new GutenbergBridgeJS2Parent.LogExceptionCallback() { + @Override public void onLogException(boolean success) { + jsCallback.invoke(success); + } + }; + } } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergJsException.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergJsException.kt new file mode 100644 index 00000000000000..243c308b5c6700 --- /dev/null +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergJsException.kt @@ -0,0 +1,61 @@ +package org.wordpress.mobile.WPAndroidGlue + +import com.facebook.react.bridge.ReadableMap + +data class JsExceptionStackTraceElement ( + val fileName: String?, + val lineNumber: Int?, + val colNumber: Int?, + val function: String, +) +class GutenbergJsException ( + val type: String, + val message: String, + var stackTrace: List, + val context: Map = emptyMap(), + val tags: Map = emptyMap(), + val isHandled: Boolean, + val handledBy: String +) { + + companion object { + @JvmStatic + fun fromReadableMap(rawException: ReadableMap): GutenbergJsException { + val type: String = rawException.getString("type") ?: "" + val message: String = rawException.getString("message") ?: "" + + val stackTrace: List = rawException.getArray("stacktrace")?.let { + (0 until it.size()).mapNotNull { index -> + val stackTraceElement = it.getMap(index) + stackTraceElement?.let { + val stackTraceFunction = stackTraceElement.getString("function") + stackTraceFunction?.let { + JsExceptionStackTraceElement( + stackTraceElement.getString("filename"), + if (stackTraceElement.hasKey("lineno")) stackTraceElement.getInt("lineno") else null, + if (stackTraceElement.hasKey("colno")) stackTraceElement.getInt("colno") else null, + stackTraceFunction + ) + } + } + } + } ?: emptyList() + + val context: Map = rawException.getMap("context")?.toHashMap() ?: emptyMap() + val tags: Map = rawException.getMap("tags")?.toHashMap()?.mapValues { it.value.toString() } ?: emptyMap() + val isHandled: Boolean = rawException.getBoolean("isHandled") + val handledBy: String = rawException.getString("handledBy") ?: "" + + return GutenbergJsException( + type, + message, + stackTrace, + context, + tags, + isHandled, + handledBy + ) + } + } +} + diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index f1b3759c568b79..8677c1737c52fb 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -18,7 +18,9 @@ import androidx.core.util.Pair; import androidx.fragment.app.Fragment; +import com.BV.LinearGradient.LinearGradientPackage; import com.brentvatne.react.ReactVideoPackage; +import com.dylanvann.fastimage.FastImageViewPackage; import com.facebook.hermes.reactexecutor.HermesExecutorFactory; import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory; import com.facebook.imagepipeline.core.ImagePipelineConfig; @@ -40,23 +42,22 @@ import com.facebook.react.shell.MainReactPackage; import com.facebook.soloader.SoLoader; import com.horcrux.svg.SvgPackage; -import com.BV.LinearGradient.LinearGradientPackage; import com.reactnativecommunity.clipboard.ClipboardPackage; import com.reactnativecommunity.slider.ReactSliderPackage; -import org.linusu.RNGetRandomValuesPackage; import com.reactnativecommunity.webview.RNCWebViewPackage; import com.swmansion.gesturehandler.RNGestureHandlerPackage; import com.swmansion.reanimated.ReanimatedPackage; import com.swmansion.rnscreens.RNScreensPackage; import com.th3rdwave.safeareacontext.SafeAreaContextPackage; -import org.reactnative.maskedview.RNCMaskedViewPackage; -import com.dylanvann.fastimage.FastImageViewPackage; +import org.linusu.RNGetRandomValuesPackage; +import org.reactnative.maskedview.RNCMaskedViewPackage; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.mobile.ReactNativeAztec.ReactAztecPackage; import org.wordpress.mobile.ReactNativeGutenbergBridge.BuildConfig; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.LogExceptionCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaSelectedCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ReplaceUnsupportedBlockCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.RNMedia; @@ -112,6 +113,8 @@ public class WPAndroidGlueCode { private OnToggleRedoButtonListener mOnToggleRedoButtonListener; private OnConnectionStatusEventListener mOnConnectionStatusEventListener; private OnBackHandlerEventListener mOnBackHandlerEventListener; + + private OnLogExceptionListener mOnLogExceptionListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -260,6 +263,10 @@ public interface OnBackHandlerEventListener { void onBackHandler(); } + public interface OnLogExceptionListener { + void onLogException(GutenbergJsException exception, LogExceptionCallback logExceptionCallback); + } + public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -561,6 +568,11 @@ public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCal boolean isConnected = mOnConnectionStatusEventListener.onRequestConnectionStatus(); connectionStatusCallback.onRequestConnectionStatus(isConnected); } + + @Override + public void logException(GutenbergJsException exception, LogExceptionCallback logExceptionCallback) { + mOnLogExceptionListener.onLogException(exception, logExceptionCallback); + } }, mIsDarkMode); return Arrays.asList( @@ -659,6 +671,7 @@ public void attachToContainer(ViewGroup viewGroup, OnToggleRedoButtonListener onToggleRedoButtonListener, OnConnectionStatusEventListener onConnectionStatusEventListener, OnBackHandlerEventListener onBackHandlerEventListener, + OnLogExceptionListener onLogExceptionListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -684,6 +697,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnToggleRedoButtonListener = onToggleRedoButtonListener; mOnConnectionStatusEventListener = onConnectionStatusEventListener; mOnBackHandlerEventListener = onBackHandlerEventListener; + mOnLogExceptionListener = onLogExceptionListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.kt b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.kt index 479d811e2d3d95..00f6d16b4ca2de 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.kt +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.kt @@ -46,6 +46,7 @@ import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent. import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergWebViewActivity import org.wordpress.mobile.ReactNativeGutenbergBridge.RNMedia import org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgePackage +import org.wordpress.mobile.WPAndroidGlue.GutenbergJsException import org.wordpress.mobile.WPAndroidGlue.Media import org.wordpress.mobile.WPAndroidGlue.MediaOption @@ -218,6 +219,8 @@ class MainApplication : Application(), ReactApplication, GutenbergBridgeInterfac override fun requestConnectionStatus(connectionStatusCallback: ConnectionStatusCallback) { connectionStatusCallback.onRequestConnectionStatus(true) } + + override fun logException(exception: GutenbergJsException, logExceptionCallback: GutenbergBridgeJS2Parent.LogExceptionCallback) {} }, isDarkMode) }