baodz-network-logger-debug-controller 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # baodz-network-logger-debug-controller
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native controller for [`react-native-network-logger`](https://www.npmjs.com/package/react-native-network-logger). It starts request capture, shows a native floating `LOG` button, toggles the button with a device shake, and opens a full-screen request viewer.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install baodz-network-logger-debug-controller react-native-network-logger react-native-safe-area-context
9
+ cd ios && pod install
10
+ ```
11
+
12
+ The package autolinks its iOS and Android native modules. It requires React Native 0.73 or later and React 18 or later.
13
+ The native module uses React Native's classic bridge; the two IVIE projects currently have the New Architecture disabled.
14
+
15
+ ## Use
16
+
17
+ Mount one controller under `SafeAreaProvider`, near the app root:
18
+
19
+ ```jsx
20
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
21
+ import { NetworkLoggerDebugController } from 'baodz-network-logger-debug-controller';
22
+
23
+ function App() {
24
+ return (
25
+ <SafeAreaProvider>
26
+ <NetworkLoggerDebugController enabled={__DEV__} />
27
+ {/* The rest of the app */}
28
+ </SafeAreaProvider>
29
+ );
30
+ }
31
+ ```
32
+
33
+ The controller starts request capture when enabled. To start capture earlier from the app entry point, call `startNetworkLogger` directly; it is safe to call more than once:
34
+
35
+ ```js
36
+ import { startNetworkLogger } from 'baodz-network-logger-debug-controller';
37
+
38
+ startNetworkLogger({ enabled: __DEV__, maxRequests: 200 });
39
+ ```
40
+
41
+ On Android, the floating button uses the system “Display over other apps” permission. Request it from a user action before mounting the controller:
42
+
43
+ ```js
44
+ import { requestOverlayPermission } from 'baodz-network-logger-debug-controller';
45
+
46
+ await requestOverlayPermission();
47
+ ```
48
+
49
+ The library manifest declares `SYSTEM_ALERT_WINDOW`; the user still needs to grant the special permission in Android Settings. iOS does not require this permission.
50
+
51
+ ## API
52
+
53
+ - `NetworkLoggerDebugController`: props `enabled` (default `true`), `maxRequests` (default `200`), and `maxRows` (default `200`).
54
+ - `startNetworkLogger({ enabled, maxRequests })`: starts capture once and returns whether it started.
55
+ - `stopNetworkLogger()`: stops capture and returns whether it stopped.
56
+ - `hasOverlayPermission()`: checks Android overlay access; resolves `true` on iOS.
57
+ - `requestOverlayPermission()`: opens the Android overlay permission settings; resolves `true` on iOS.
58
+
59
+ ## Publish
60
+
61
+ From the package directory, authenticate to the intended npm registry and publish the version in `package.json`:
62
+
63
+ ```sh
64
+ npm login
65
+ npm publish
66
+ ```
67
+
68
+ The package name is unscoped, so the registry must allow publishing `baodz-network-logger-debug-controller`. The podspec is intended for React Native autolinking from the installed npm package; add a repository `s.source` if you later want to distribute this as a standalone CocoaPods pod.
69
+
70
+ The package metadata is marked `UNLICENSED` because no distribution license was specified. Set the intended license before publishing it to a public registry.
@@ -0,0 +1,41 @@
1
+ buildscript {
2
+ if (project == rootProject) {
3
+ repositories {
4
+ google()
5
+ mavenCentral()
6
+ }
7
+
8
+ dependencies {
9
+ classpath 'com.android.tools.build:gradle:8.2.1'
10
+ }
11
+ }
12
+ }
13
+
14
+ apply plugin: 'com.android.library'
15
+
16
+ def safeExtGet(name, fallback) {
17
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
18
+ }
19
+
20
+ android {
21
+ namespace 'com.baodz.networkloggerdebugcontroller'
22
+ compileSdkVersion safeExtGet('compileSdkVersion', 34)
23
+
24
+ defaultConfig {
25
+ minSdkVersion safeExtGet('minSdkVersion', 21)
26
+ }
27
+
28
+ compileOptions {
29
+ sourceCompatibility JavaVersion.VERSION_1_8
30
+ targetCompatibility JavaVersion.VERSION_1_8
31
+ }
32
+ }
33
+
34
+ repositories {
35
+ google()
36
+ mavenCentral()
37
+ }
38
+
39
+ dependencies {
40
+ implementation 'com.facebook.react:react-android'
41
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
3
+ </manifest>
@@ -0,0 +1,377 @@
1
+ package com.baodz.networkloggerdebugcontroller;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import android.graphics.Color;
7
+ import android.graphics.PixelFormat;
8
+ import android.graphics.Rect;
9
+ import android.graphics.drawable.GradientDrawable;
10
+ import android.hardware.Sensor;
11
+ import android.hardware.SensorEvent;
12
+ import android.hardware.SensorEventListener;
13
+ import android.hardware.SensorManager;
14
+ import android.net.Uri;
15
+ import android.os.Build;
16
+ import android.os.Handler;
17
+ import android.os.Looper;
18
+ import android.provider.Settings;
19
+ import android.view.Gravity;
20
+ import android.view.MotionEvent;
21
+ import android.view.View;
22
+ import android.view.ViewConfiguration;
23
+ import android.view.WindowManager;
24
+ import android.util.DisplayMetrics;
25
+ import android.widget.TextView;
26
+
27
+ import com.facebook.react.bridge.ActivityEventListener;
28
+ import com.facebook.react.bridge.BaseActivityEventListener;
29
+ import com.facebook.react.bridge.Promise;
30
+ import com.facebook.react.bridge.ReactApplicationContext;
31
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
32
+ import com.facebook.react.bridge.ReactMethod;
33
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
34
+
35
+ public class BaodzNetworkLoggerDebugControllerModule extends ReactContextBaseJavaModule
36
+ implements SensorEventListener {
37
+ private static final String EVENT_BUTTON_PRESS = "NetworkLoggerButtonPress";
38
+ private static final int OVERLAY_PERMISSION_REQUEST_CODE = 5678;
39
+ private static final int BUTTON_SIZE_DP = 52;
40
+ private static final float SHAKE_THRESHOLD_G = 2.7f;
41
+ private static final long SHAKE_DEBOUNCE_MS = 900L;
42
+
43
+ private final ReactApplicationContext reactContext;
44
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
45
+ private final SensorManager sensorManager;
46
+ private final Sensor accelerometer;
47
+ private final WindowManager windowManager;
48
+ private TextView button;
49
+ private WindowManager.LayoutParams buttonLayoutParams;
50
+ private int buttonX;
51
+ private int buttonY;
52
+ private boolean buttonVisible;
53
+ private boolean shakeToggleStarted;
54
+ private long lastShakeAt;
55
+ private Promise overlayPermissionPromise;
56
+
57
+ private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
58
+ @Override
59
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
60
+ if (requestCode != OVERLAY_PERMISSION_REQUEST_CODE || overlayPermissionPromise == null) {
61
+ return;
62
+ }
63
+
64
+ overlayPermissionPromise.resolve(canDrawOverlays());
65
+ overlayPermissionPromise = null;
66
+ }
67
+ };
68
+
69
+ BaodzNetworkLoggerDebugControllerModule(ReactApplicationContext context) {
70
+ super(context);
71
+ reactContext = context;
72
+ sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
73
+ accelerometer = sensorManager == null
74
+ ? null
75
+ : sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
76
+ windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
77
+ buttonX = dp(16);
78
+ buttonY = dp(144);
79
+ context.addActivityEventListener(activityEventListener);
80
+ }
81
+
82
+ @Override
83
+ public String getName() {
84
+ return "BaodzNetworkLoggerDebugController";
85
+ }
86
+
87
+ private int dp(int value) {
88
+ return Math.round(value * reactContext.getResources().getDisplayMetrics().density);
89
+ }
90
+
91
+ private boolean canDrawOverlays() {
92
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(reactContext);
93
+ }
94
+
95
+ private int getOverlayWindowType() {
96
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
97
+ return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
98
+ }
99
+ return WindowManager.LayoutParams.TYPE_PHONE;
100
+ }
101
+
102
+ private WindowManager.LayoutParams createButtonLayoutParams() {
103
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
104
+ dp(BUTTON_SIZE_DP),
105
+ dp(BUTTON_SIZE_DP),
106
+ getOverlayWindowType(),
107
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
108
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
109
+ PixelFormat.TRANSLUCENT);
110
+ params.gravity = Gravity.TOP | Gravity.START;
111
+ params.x = buttonX;
112
+ params.y = buttonY;
113
+ return params;
114
+ }
115
+
116
+ private int[] getScreenSize() {
117
+ if (windowManager == null) {
118
+ return new int[] {0, 0};
119
+ }
120
+
121
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
122
+ Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
123
+ return new int[] {bounds.width(), bounds.height()};
124
+ }
125
+
126
+ DisplayMetrics metrics = new DisplayMetrics();
127
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
128
+ return new int[] {metrics.widthPixels, metrics.heightPixels};
129
+ }
130
+
131
+ private int[] clampButtonPosition(int requestedX, int requestedY) {
132
+ int[] screenSize = getScreenSize();
133
+ int buttonSize = dp(BUTTON_SIZE_DP);
134
+ int maxX = Math.max(0, screenSize[0] - buttonSize);
135
+ int maxY = Math.max(0, screenSize[1] - buttonSize);
136
+ return new int[] {
137
+ Math.max(0, Math.min(requestedX, maxX)),
138
+ Math.max(0, Math.min(requestedY, maxY))
139
+ };
140
+ }
141
+
142
+ private TextView createButton() {
143
+ TextView view = new TextView(reactContext);
144
+ view.setText("LOG");
145
+ view.setTextColor(Color.WHITE);
146
+ view.setTextSize(10);
147
+ view.setGravity(Gravity.CENTER);
148
+ view.setElevation(dp(8));
149
+
150
+ GradientDrawable background = new GradientDrawable();
151
+ background.setColor(Color.rgb(17, 24, 39));
152
+ background.setShape(GradientDrawable.OVAL);
153
+ background.setCornerRadius(dp(BUTTON_SIZE_DP) / 2f);
154
+ background.setStroke(dp(1), Color.rgb(52, 211, 153));
155
+ view.setBackground(background);
156
+ view.setTypeface(view.getTypeface(), android.graphics.Typeface.BOLD);
157
+ view.setOnClickListener(clickedView -> emitButtonPress());
158
+ enableDragging(view);
159
+ return view;
160
+ }
161
+
162
+ private void enableDragging(TextView view) {
163
+ view.setOnTouchListener(new View.OnTouchListener() {
164
+ private float startRawX;
165
+ private float startRawY;
166
+ private int startX;
167
+ private int startY;
168
+ private boolean dragged;
169
+ private final int touchSlop = ViewConfiguration.get(reactContext).getScaledTouchSlop();
170
+
171
+ @Override
172
+ public boolean onTouch(View touchedView, MotionEvent event) {
173
+ switch (event.getActionMasked()) {
174
+ case MotionEvent.ACTION_DOWN:
175
+ startRawX = event.getRawX();
176
+ startRawY = event.getRawY();
177
+ startX = buttonX;
178
+ startY = buttonY;
179
+ dragged = false;
180
+ return true;
181
+ case MotionEvent.ACTION_MOVE:
182
+ float deltaX = event.getRawX() - startRawX;
183
+ float deltaY = event.getRawY() - startRawY;
184
+ if (Math.hypot(deltaX, deltaY) >= touchSlop) {
185
+ dragged = true;
186
+ }
187
+ if (dragged) {
188
+ updateButtonPosition(startX + Math.round(deltaX), startY + Math.round(deltaY));
189
+ }
190
+ return true;
191
+ case MotionEvent.ACTION_UP:
192
+ if (!dragged) {
193
+ touchedView.performClick();
194
+ }
195
+ return true;
196
+ case MotionEvent.ACTION_CANCEL:
197
+ return true;
198
+ default:
199
+ return false;
200
+ }
201
+ }
202
+ });
203
+ }
204
+
205
+ private void updateButtonPosition(int x, int y) {
206
+ if (windowManager == null || button == null || buttonLayoutParams == null) {
207
+ return;
208
+ }
209
+
210
+ int[] position = clampButtonPosition(x, y);
211
+ buttonX = position[0];
212
+ buttonY = position[1];
213
+ buttonLayoutParams.x = buttonX;
214
+ buttonLayoutParams.y = buttonY;
215
+ try {
216
+ windowManager.updateViewLayout(button, buttonLayoutParams);
217
+ } catch (IllegalArgumentException | SecurityException ignored) {
218
+ hideNetworkLoggerButtonOnMain();
219
+ }
220
+ }
221
+
222
+ private void showNetworkLoggerButtonOnMain() {
223
+ if (!canDrawOverlays() || windowManager == null || buttonVisible) {
224
+ return;
225
+ }
226
+
227
+ button = createButton();
228
+ buttonLayoutParams = createButtonLayoutParams();
229
+ try {
230
+ windowManager.addView(button, buttonLayoutParams);
231
+ buttonVisible = true;
232
+ } catch (IllegalArgumentException | SecurityException ignored) {
233
+ button = null;
234
+ buttonLayoutParams = null;
235
+ buttonVisible = false;
236
+ }
237
+ }
238
+
239
+ private void hideNetworkLoggerButtonOnMain() {
240
+ if (windowManager != null && button != null) {
241
+ try {
242
+ windowManager.removeView(button);
243
+ } catch (IllegalArgumentException | SecurityException ignored) {
244
+ // The system can remove an application overlay when permission changes.
245
+ }
246
+ }
247
+ button = null;
248
+ buttonLayoutParams = null;
249
+ buttonVisible = false;
250
+ }
251
+
252
+ private void toggleNetworkLoggerButton() {
253
+ mainHandler.post(() -> {
254
+ if (buttonVisible) {
255
+ hideNetworkLoggerButtonOnMain();
256
+ } else {
257
+ showNetworkLoggerButtonOnMain();
258
+ }
259
+ });
260
+ }
261
+
262
+ private void emitButtonPress() {
263
+ if (!reactContext.hasActiveCatalystInstance()) {
264
+ return;
265
+ }
266
+ reactContext
267
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
268
+ .emit(EVENT_BUTTON_PRESS, null);
269
+ }
270
+
271
+ @ReactMethod
272
+ public void showNetworkLoggerButton() {
273
+ mainHandler.post(this::showNetworkLoggerButtonOnMain);
274
+ }
275
+
276
+ @ReactMethod
277
+ public void hideNetworkLoggerButton() {
278
+ mainHandler.post(this::hideNetworkLoggerButtonOnMain);
279
+ }
280
+
281
+ @ReactMethod
282
+ public void startNetworkLoggerShakeToggle(boolean initiallyVisible) {
283
+ mainHandler.post(() -> {
284
+ if (initiallyVisible) {
285
+ showNetworkLoggerButtonOnMain();
286
+ } else {
287
+ hideNetworkLoggerButtonOnMain();
288
+ }
289
+ });
290
+
291
+ if (sensorManager != null && accelerometer != null && !shakeToggleStarted) {
292
+ sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
293
+ shakeToggleStarted = true;
294
+ }
295
+ }
296
+
297
+ @ReactMethod
298
+ public void stopNetworkLoggerShakeToggle() {
299
+ if (sensorManager != null && shakeToggleStarted) {
300
+ sensorManager.unregisterListener(this);
301
+ }
302
+ shakeToggleStarted = false;
303
+ mainHandler.post(this::hideNetworkLoggerButtonOnMain);
304
+ }
305
+
306
+ @ReactMethod
307
+ public void addListener(String eventName) {
308
+ // Required by NativeEventEmitter on Android.
309
+ }
310
+
311
+ @ReactMethod
312
+ public void removeListeners(Integer count) {
313
+ // Required by NativeEventEmitter on Android.
314
+ }
315
+
316
+ @ReactMethod
317
+ public void hasOverlayPermission(Promise promise) {
318
+ promise.resolve(canDrawOverlays());
319
+ }
320
+
321
+ @ReactMethod
322
+ public void requestOverlayPermission(Promise promise) {
323
+ if (canDrawOverlays()) {
324
+ promise.resolve(true);
325
+ return;
326
+ }
327
+
328
+ Activity activity = getCurrentActivity();
329
+ if (activity == null) {
330
+ promise.resolve(false);
331
+ return;
332
+ }
333
+
334
+ if (overlayPermissionPromise != null) {
335
+ overlayPermissionPromise.reject("E_OVERLAY_PERMISSION", "A permission request is already active.");
336
+ }
337
+ overlayPermissionPromise = promise;
338
+
339
+ Intent intent = new Intent(
340
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
341
+ Uri.parse("package:" + reactContext.getPackageName()));
342
+ try {
343
+ activity.startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE);
344
+ } catch (RuntimeException error) {
345
+ overlayPermissionPromise = null;
346
+ promise.reject("E_OVERLAY_PERMISSION", error);
347
+ }
348
+ }
349
+
350
+ @Override
351
+ public void onSensorChanged(SensorEvent event) {
352
+ if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
353
+ return;
354
+ }
355
+
356
+ float gx = event.values[0] / SensorManager.GRAVITY_EARTH;
357
+ float gy = event.values[1] / SensorManager.GRAVITY_EARTH;
358
+ float gz = event.values[2] / SensorManager.GRAVITY_EARTH;
359
+ double gForce = Math.sqrt(gx * gx + gy * gy + gz * gz);
360
+ if (gForce < SHAKE_THRESHOLD_G) {
361
+ return;
362
+ }
363
+
364
+ long now = System.currentTimeMillis();
365
+ if (now - lastShakeAt < SHAKE_DEBOUNCE_MS) {
366
+ return;
367
+ }
368
+
369
+ lastShakeAt = now;
370
+ toggleNetworkLoggerButton();
371
+ }
372
+
373
+ @Override
374
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
375
+ // No accuracy adjustment is needed for shake detection.
376
+ }
377
+ }
@@ -0,0 +1,24 @@
1
+ package com.baodz.networkloggerdebugcontroller;
2
+
3
+ import com.facebook.react.ReactPackage;
4
+ import com.facebook.react.bridge.NativeModule;
5
+ import com.facebook.react.bridge.ReactApplicationContext;
6
+ import com.facebook.react.uimanager.ViewManager;
7
+
8
+ import java.util.ArrayList;
9
+ import java.util.Collections;
10
+ import java.util.List;
11
+
12
+ public class BaodzNetworkLoggerDebugControllerPackage implements ReactPackage {
13
+ @Override
14
+ public List<NativeModule> createNativeModules(ReactApplicationContext context) {
15
+ List<NativeModule> modules = new ArrayList<>();
16
+ modules.add(new BaodzNetworkLoggerDebugControllerModule(context));
17
+ return modules;
18
+ }
19
+
20
+ @Override
21
+ public List<ViewManager> createViewManagers(ReactApplicationContext context) {
22
+ return Collections.emptyList();
23
+ }
24
+ }
@@ -0,0 +1,16 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'BaodzNetworkLoggerDebugController'
3
+ s.version = '0.1.0'
4
+ s.summary = 'React Native network logger debug controller.'
5
+ s.description = 'A React Native debug controller with native shake-toggle floating buttons.'
6
+ s.homepage = 'https://www.npmjs.com/package/baodz-network-logger-debug-controller'
7
+ s.license = { :type => 'UNLICENSED' }
8
+ s.author = 'baodz'
9
+ # React Native autolinking consumes this podspec from the installed npm package path.
10
+ s.platforms = { :ios => '12.0' }
11
+ s.source_files = 'ios/**/*.{h,m,mm}'
12
+ s.requires_arc = true
13
+
14
+ s.frameworks = 'CoreMotion', 'UIKit'
15
+ s.dependency 'React-Core'
16
+ end
@@ -0,0 +1,5 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+
4
+ @interface BaodzNetworkLoggerDebugController : RCTEventEmitter <RCTBridgeModule>
5
+ @end
@@ -0,0 +1,234 @@
1
+ #import "BaodzNetworkLoggerDebugController.h"
2
+
3
+ #import <CoreMotion/CoreMotion.h>
4
+ #import <UIKit/UIKit.h>
5
+ #import <math.h>
6
+
7
+ static NSString *const NetworkLoggerButtonPressEvent = @"NetworkLoggerButtonPress";
8
+ static NSTimeInterval const ShakeDebounceSeconds = 0.9;
9
+ static double const ShakeThresholdG = 2.7;
10
+ static CGFloat const NetworkLoggerButtonSize = 52.0;
11
+
12
+ @interface BaodzNetworkLoggerDebugController ()
13
+ @property (nonatomic, strong) CMMotionManager *motionManager;
14
+ @property (nonatomic, strong) UIWindow *networkLoggerWindow;
15
+ @property (nonatomic, assign) BOOL networkLoggerButtonVisible;
16
+ @property (nonatomic, assign) NSTimeInterval lastShakeAt;
17
+ @end
18
+
19
+ @implementation BaodzNetworkLoggerDebugController
20
+
21
+ RCT_EXPORT_MODULE(BaodzNetworkLoggerDebugController);
22
+
23
+ - (dispatch_queue_t)methodQueue
24
+ {
25
+ return dispatch_get_main_queue();
26
+ }
27
+
28
+ - (NSArray<NSString *> *)supportedEvents
29
+ {
30
+ return @[NetworkLoggerButtonPressEvent];
31
+ }
32
+
33
+ - (instancetype)init
34
+ {
35
+ self = [super init];
36
+ if (self) {
37
+ _motionManager = [CMMotionManager new];
38
+ _motionManager.accelerometerUpdateInterval = 0.12;
39
+ }
40
+ return self;
41
+ }
42
+
43
+ - (UIWindowScene *)activeWindowScene
44
+ {
45
+ if (@available(iOS 13.0, *)) {
46
+ for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
47
+ if (scene.activationState == UISceneActivationStateForegroundActive &&
48
+ [scene isKindOfClass:UIWindowScene.class]) {
49
+ return (UIWindowScene *)scene;
50
+ }
51
+ }
52
+ }
53
+ return nil;
54
+ }
55
+
56
+ - (UIWindow *)makeNetworkLoggerWindow
57
+ {
58
+ CGRect frame = CGRectMake(16, 144, NetworkLoggerButtonSize, NetworkLoggerButtonSize);
59
+ UIWindow *window;
60
+ if (@available(iOS 13.0, *)) {
61
+ UIWindowScene *windowScene = [self activeWindowScene];
62
+ if (windowScene != nil) {
63
+ window = [[UIWindow alloc] initWithWindowScene:windowScene];
64
+ window.frame = frame;
65
+ } else {
66
+ window = [[UIWindow alloc] initWithFrame:frame];
67
+ }
68
+ } else {
69
+ window = [[UIWindow alloc] initWithFrame:frame];
70
+ }
71
+
72
+ window.windowLevel = UIWindowLevelAlert + 2;
73
+ window.backgroundColor = UIColor.clearColor;
74
+ window.hidden = YES;
75
+
76
+ UIViewController *viewController = [UIViewController new];
77
+ viewController.view.backgroundColor = UIColor.clearColor;
78
+
79
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
80
+ button.frame = CGRectMake(0, 0, NetworkLoggerButtonSize, NetworkLoggerButtonSize);
81
+ button.backgroundColor = [UIColor colorWithRed:17.0 / 255.0 green:24.0 / 255.0 blue:39.0 / 255.0 alpha:1.0];
82
+ button.layer.cornerRadius = NetworkLoggerButtonSize / 2.0;
83
+ button.layer.borderColor = [UIColor colorWithRed:52.0 / 255.0 green:211.0 / 255.0 blue:153.0 / 255.0 alpha:1.0].CGColor;
84
+ button.layer.borderWidth = 1;
85
+ button.titleLabel.font = [UIFont boldSystemFontOfSize:10];
86
+ [button setTitle:@"LOG" forState:UIControlStateNormal];
87
+ [button setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
88
+ [button addTarget:self action:@selector(onNetworkLoggerButtonPress) forControlEvents:UIControlEventTouchUpInside];
89
+
90
+ UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self
91
+ action:@selector(handleNetworkLoggerPan:)];
92
+ [button addGestureRecognizer:panGesture];
93
+ [viewController.view addSubview:button];
94
+
95
+ window.rootViewController = viewController;
96
+ return window;
97
+ }
98
+
99
+ - (void)ensureNetworkLoggerWindow
100
+ {
101
+ if (self.networkLoggerWindow == nil) {
102
+ self.networkLoggerWindow = [self makeNetworkLoggerWindow];
103
+ }
104
+ }
105
+
106
+ - (void)showNetworkLoggerButtonOnMain
107
+ {
108
+ [self ensureNetworkLoggerWindow];
109
+ self.networkLoggerWindow.hidden = NO;
110
+ self.networkLoggerButtonVisible = YES;
111
+ }
112
+
113
+ - (void)hideNetworkLoggerButtonOnMain
114
+ {
115
+ self.networkLoggerWindow.hidden = YES;
116
+ self.networkLoggerButtonVisible = NO;
117
+ }
118
+
119
+ - (void)toggleNetworkLoggerButton
120
+ {
121
+ if (self.networkLoggerButtonVisible) {
122
+ [self hideNetworkLoggerButtonOnMain];
123
+ } else {
124
+ [self showNetworkLoggerButtonOnMain];
125
+ }
126
+ }
127
+
128
+ - (CGRect)networkLoggerMovementBounds
129
+ {
130
+ if (@available(iOS 13.0, *)) {
131
+ UIWindowScene *windowScene = self.networkLoggerWindow.windowScene ?: [self activeWindowScene];
132
+ if (windowScene != nil) {
133
+ return windowScene.coordinateSpace.bounds;
134
+ }
135
+ }
136
+ return UIScreen.mainScreen.bounds;
137
+ }
138
+
139
+ - (CGRect)clampedNetworkLoggerFrame:(CGRect)frame
140
+ {
141
+ CGRect bounds = [self networkLoggerMovementBounds];
142
+ CGFloat maxX = MAX(CGRectGetMinX(bounds), CGRectGetMaxX(bounds) - CGRectGetWidth(frame));
143
+ CGFloat maxY = MAX(CGRectGetMinY(bounds), CGRectGetMaxY(bounds) - CGRectGetHeight(frame));
144
+ frame.origin.x = MIN(MAX(CGRectGetMinX(bounds), frame.origin.x), maxX);
145
+ frame.origin.y = MIN(MAX(CGRectGetMinY(bounds), frame.origin.y), maxY);
146
+ return frame;
147
+ }
148
+
149
+ - (void)handleNetworkLoggerPan:(UIPanGestureRecognizer *)gesture
150
+ {
151
+ if (self.networkLoggerWindow == nil) {
152
+ return;
153
+ }
154
+
155
+ CGPoint translation = [gesture translationInView:nil];
156
+ CGRect frame = CGRectOffset(self.networkLoggerWindow.frame, translation.x, translation.y);
157
+ self.networkLoggerWindow.frame = [self clampedNetworkLoggerFrame:frame];
158
+ [gesture setTranslation:CGPointZero inView:nil];
159
+ }
160
+
161
+ - (void)onNetworkLoggerButtonPress
162
+ {
163
+ [self sendEventWithName:NetworkLoggerButtonPressEvent body:nil];
164
+ }
165
+
166
+ RCT_EXPORT_METHOD(showNetworkLoggerButton)
167
+ {
168
+ [self showNetworkLoggerButtonOnMain];
169
+ }
170
+
171
+ RCT_EXPORT_METHOD(hideNetworkLoggerButton)
172
+ {
173
+ [self hideNetworkLoggerButtonOnMain];
174
+ }
175
+
176
+ RCT_EXPORT_METHOD(startNetworkLoggerShakeToggle:(BOOL)initiallyVisible)
177
+ {
178
+ if (initiallyVisible) {
179
+ [self showNetworkLoggerButtonOnMain];
180
+ } else {
181
+ [self hideNetworkLoggerButtonOnMain];
182
+ }
183
+
184
+ if (!self.motionManager.accelerometerAvailable || self.motionManager.accelerometerActive) {
185
+ return;
186
+ }
187
+
188
+ __weak BaodzNetworkLoggerDebugController *weakSelf = self;
189
+ [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
190
+ withHandler:^(CMAccelerometerData *data, NSError *error) {
191
+ BaodzNetworkLoggerDebugController *strongSelf = weakSelf;
192
+ if (strongSelf == nil || data == nil || error != nil) {
193
+ return;
194
+ }
195
+
196
+ double x = data.acceleration.x;
197
+ double y = data.acceleration.y;
198
+ double z = data.acceleration.z;
199
+ double gForce = sqrt(x * x + y * y + z * z);
200
+ if (gForce < ShakeThresholdG) {
201
+ return;
202
+ }
203
+
204
+ NSTimeInterval now = NSDate.date.timeIntervalSince1970;
205
+ if (now - strongSelf.lastShakeAt < ShakeDebounceSeconds) {
206
+ return;
207
+ }
208
+
209
+ strongSelf.lastShakeAt = now;
210
+ [strongSelf toggleNetworkLoggerButton];
211
+ }];
212
+ }
213
+
214
+ RCT_EXPORT_METHOD(stopNetworkLoggerShakeToggle)
215
+ {
216
+ if (self.motionManager.accelerometerActive) {
217
+ [self.motionManager stopAccelerometerUpdates];
218
+ }
219
+ [self hideNetworkLoggerButtonOnMain];
220
+ }
221
+
222
+ RCT_EXPORT_METHOD(hasOverlayPermission:(RCTPromiseResolveBlock)resolve
223
+ rejecter:(RCTPromiseRejectBlock)reject)
224
+ {
225
+ resolve(@YES);
226
+ }
227
+
228
+ RCT_EXPORT_METHOD(requestOverlayPermission:(RCTPromiseResolveBlock)resolve
229
+ rejecter:(RCTPromiseRejectBlock)reject)
230
+ {
231
+ resolve(@YES);
232
+ }
233
+
234
+ @end
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "baodz-network-logger-debug-controller",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "React Native network logger debug controller with a shake-toggle floating button.",
5
+ "main": "src/index.js",
6
+ "react-native": "src/index.js",
7
+ "types": "types/index.d.ts",
8
+ "files": [
9
+ "android/",
10
+ "ios/",
11
+ "src/",
12
+ "types/",
13
+ "*.podspec",
14
+ "react-native.config.js",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "pack:check": "npm pack --dry-run"
19
+ },
20
+ "keywords": [
21
+ "react-native",
22
+ "network-logger",
23
+ "debug"
24
+ ],
25
+ "peerDependencies": {
26
+ "react": ">=18.2.0",
27
+ "react-native": ">=0.73.0",
28
+ "react-native-network-logger": ">=3.0.0 <4.0.0",
29
+ "react-native-safe-area-context": ">=4.10.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "license": "UNLICENSED"
35
+ }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageImportPath:
6
+ 'import com.baodz.networkloggerdebugcontroller.BaodzNetworkLoggerDebugControllerPackage;',
7
+ packageInstance:
8
+ 'new BaodzNetworkLoggerDebugControllerPackage()',
9
+ },
10
+ ios: {},
11
+ },
12
+ },
13
+ };
package/src/index.js ADDED
@@ -0,0 +1,151 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ StyleSheet,
5
+ Text,
6
+ TouchableOpacity,
7
+ View,
8
+ } from 'react-native';
9
+ import NetworkLogger, {
10
+ startNetworkLogging,
11
+ stopNetworkLogging,
12
+ } from 'react-native-network-logger';
13
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
+
15
+ import {
16
+ hasOverlayPermission,
17
+ hideNetworkLoggerButton,
18
+ NETWORK_LOGGER_BUTTON_PRESS_EVENT,
19
+ requestOverlayPermission,
20
+ showNetworkLoggerButton,
21
+ startNetworkLoggerShakeToggle,
22
+ stopNetworkLoggerShakeToggle,
23
+ subscribeNetworkLoggerButtonPress,
24
+ } from './native-module';
25
+
26
+ export const NETWORK_LOGGER_MAX_REQUESTS = 200;
27
+
28
+ let networkLoggingStarted = false;
29
+
30
+ export const startNetworkLogger = ({
31
+ enabled = true,
32
+ maxRequests = NETWORK_LOGGER_MAX_REQUESTS,
33
+ } = {}) => {
34
+ if (!enabled || networkLoggingStarted) return false;
35
+
36
+ startNetworkLogging({ maxRequests });
37
+ networkLoggingStarted = true;
38
+ return true;
39
+ };
40
+
41
+ export const stopNetworkLogger = () => {
42
+ if (!networkLoggingStarted) return false;
43
+
44
+ stopNetworkLogging();
45
+ networkLoggingStarted = false;
46
+ return true;
47
+ };
48
+
49
+ export { hasOverlayPermission, requestOverlayPermission };
50
+ export { NETWORK_LOGGER_BUTTON_PRESS_EVENT };
51
+
52
+ export const NetworkLoggerDebugController = ({
53
+ enabled = true,
54
+ maxRequests = NETWORK_LOGGER_MAX_REQUESTS,
55
+ maxRows = NETWORK_LOGGER_MAX_REQUESTS,
56
+ } = {}) => {
57
+ const insets = useSafeAreaInsets();
58
+ const [visible, setVisible] = useState(false);
59
+
60
+ useEffect(() => {
61
+ if (!enabled) return undefined;
62
+
63
+ startNetworkLogger({ maxRequests });
64
+ startNetworkLoggerShakeToggle({ initiallyVisible: true });
65
+ const subscription = subscribeNetworkLoggerButtonPress(() => {
66
+ hideNetworkLoggerButton();
67
+ setVisible(true);
68
+ });
69
+
70
+ return () => {
71
+ subscription?.remove?.();
72
+ stopNetworkLoggerShakeToggle();
73
+ stopNetworkLogger();
74
+ };
75
+ }, [enabled, maxRequests]);
76
+
77
+ const closeViewer = () => {
78
+ setVisible(false);
79
+ if (enabled) showNetworkLoggerButton();
80
+ };
81
+
82
+ if (!enabled) return null;
83
+
84
+ return (
85
+ <Modal
86
+ animationType="slide"
87
+ onRequestClose={closeViewer}
88
+ presentationStyle="fullScreen"
89
+ statusBarTranslucent
90
+ transparent={false}
91
+ visible={visible}
92
+ >
93
+ <View
94
+ style={[
95
+ styles.panel,
96
+ { paddingBottom: insets.bottom, paddingTop: insets.top },
97
+ ]}
98
+ >
99
+ <View style={styles.header}>
100
+ <Text style={styles.title}>Network Logs</Text>
101
+ <TouchableOpacity
102
+ accessibilityLabel="Đóng network logs"
103
+ accessibilityRole="button"
104
+ onPress={closeViewer}
105
+ style={styles.closeButton}
106
+ >
107
+ <Text style={styles.closeText}>Đóng</Text>
108
+ </TouchableOpacity>
109
+ </View>
110
+ <View style={styles.logger}>
111
+ <NetworkLogger compact maxRows={maxRows} sort="desc" theme="dark" />
112
+ </View>
113
+ </View>
114
+ </Modal>
115
+ );
116
+ };
117
+
118
+ const styles = StyleSheet.create({
119
+ panel: {
120
+ backgroundColor: '#111827',
121
+ flex: 1,
122
+ },
123
+ header: {
124
+ alignItems: 'center',
125
+ borderBottomColor: '#374151',
126
+ borderBottomWidth: StyleSheet.hairlineWidth,
127
+ flexDirection: 'row',
128
+ justifyContent: 'space-between',
129
+ paddingHorizontal: 16,
130
+ paddingVertical: 12,
131
+ },
132
+ title: {
133
+ color: '#FFFFFF',
134
+ fontSize: 16,
135
+ fontWeight: '700',
136
+ },
137
+ closeButton: {
138
+ paddingHorizontal: 8,
139
+ paddingVertical: 6,
140
+ },
141
+ closeText: {
142
+ color: '#34D399',
143
+ fontSize: 15,
144
+ fontWeight: '600',
145
+ },
146
+ logger: {
147
+ flex: 1,
148
+ },
149
+ });
150
+
151
+ export default NetworkLoggerDebugController;
@@ -0,0 +1,54 @@
1
+ import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
+
3
+ const MODULE_NAME = 'BaodzNetworkLoggerDebugController';
4
+ const nativeModule = NativeModules[MODULE_NAME];
5
+ export const NETWORK_LOGGER_BUTTON_PRESS_EVENT = 'NetworkLoggerButtonPress';
6
+
7
+ const callNative = (methodName, ...args) => {
8
+ const method = nativeModule?.[methodName];
9
+ if (typeof method !== 'function') return false;
10
+
11
+ method(...args);
12
+ return true;
13
+ };
14
+
15
+ export const showNetworkLoggerButton = () =>
16
+ callNative('showNetworkLoggerButton');
17
+
18
+ export const hideNetworkLoggerButton = () =>
19
+ callNative('hideNetworkLoggerButton');
20
+
21
+ export const startNetworkLoggerShakeToggle = ({
22
+ initiallyVisible = false,
23
+ } = {}) =>
24
+ callNative('startNetworkLoggerShakeToggle', Boolean(initiallyVisible));
25
+
26
+ export const stopNetworkLoggerShakeToggle = () =>
27
+ callNative('stopNetworkLoggerShakeToggle');
28
+
29
+ export const subscribeNetworkLoggerButtonPress = (listener) => {
30
+ if (!nativeModule) return null;
31
+
32
+ return new NativeEventEmitter(nativeModule).addListener(
33
+ NETWORK_LOGGER_BUTTON_PRESS_EVENT,
34
+ listener,
35
+ );
36
+ };
37
+
38
+ export const hasOverlayPermission = async () => {
39
+ if (Platform.OS !== 'android') return true;
40
+
41
+ const method = nativeModule?.hasOverlayPermission;
42
+ if (typeof method !== 'function') return false;
43
+
44
+ return method();
45
+ };
46
+
47
+ export const requestOverlayPermission = async () => {
48
+ if (Platform.OS !== 'android') return true;
49
+
50
+ const method = nativeModule?.requestOverlayPermission;
51
+ if (typeof method !== 'function') return false;
52
+
53
+ return method();
54
+ };
@@ -0,0 +1,23 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ export interface NetworkLoggerDebugControllerProps {
4
+ /** Enable request capture, the floating button, and the viewer. */
5
+ enabled?: boolean;
6
+ /** Maximum number of requests retained by react-native-network-logger. */
7
+ maxRequests?: number;
8
+ /** Maximum number of request rows rendered in the viewer. */
9
+ maxRows?: number;
10
+ }
11
+
12
+ export declare const NETWORK_LOGGER_MAX_REQUESTS: number;
13
+ export declare const NETWORK_LOGGER_BUTTON_PRESS_EVENT: string;
14
+ export declare const NetworkLoggerDebugController: ComponentType<NetworkLoggerDebugControllerProps>;
15
+ export declare const startNetworkLogger: (options?: {
16
+ enabled?: boolean;
17
+ maxRequests?: number;
18
+ }) => boolean;
19
+ export declare const stopNetworkLogger: () => boolean;
20
+ export declare const hasOverlayPermission: () => Promise<boolean>;
21
+ export declare const requestOverlayPermission: () => Promise<boolean>;
22
+
23
+ export default NetworkLoggerDebugController;