react-native-bug-reporter 1.0.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/android/build.gradle +49 -0
  4. package/android/src/main/AndroidManifest.xml +11 -0
  5. package/android/src/main/java/com/bugreporter/ScreenshotDetectorModule.kt +227 -0
  6. package/android/src/main/java/com/bugreporter/ScreenshotDetectorPackage.kt +20 -0
  7. package/ios/RNBugReporterScreenshot.m +11 -0
  8. package/ios/RNBugReporterScreenshot.swift +113 -0
  9. package/lib/commonjs/BugReporterProvider.js +139 -0
  10. package/lib/commonjs/BugReporterProvider.js.map +1 -0
  11. package/lib/commonjs/collectors/appInfo.js +21 -0
  12. package/lib/commonjs/collectors/appInfo.js.map +1 -0
  13. package/lib/commonjs/collectors/collectContext.js +45 -0
  14. package/lib/commonjs/collectors/collectContext.js.map +1 -0
  15. package/lib/commonjs/collectors/deviceInfo.js +41 -0
  16. package/lib/commonjs/collectors/deviceInfo.js.map +1 -0
  17. package/lib/commonjs/collectors/networkInfo.js +33 -0
  18. package/lib/commonjs/collectors/networkInfo.js.map +1 -0
  19. package/lib/commonjs/components/BugReportAdminScreen.js +225 -0
  20. package/lib/commonjs/components/BugReportAdminScreen.js.map +1 -0
  21. package/lib/commonjs/components/BugReportModal.js +341 -0
  22. package/lib/commonjs/components/BugReportModal.js.map +1 -0
  23. package/lib/commonjs/components/ScreenshotEditor.js +466 -0
  24. package/lib/commonjs/components/ScreenshotEditor.js.map +1 -0
  25. package/lib/commonjs/components/ScreenshotPreview.js +134 -0
  26. package/lib/commonjs/components/ScreenshotPreview.js.map +1 -0
  27. package/lib/commonjs/components/SeveritySelector.js +65 -0
  28. package/lib/commonjs/components/SeveritySelector.js.map +1 -0
  29. package/lib/commonjs/context/BugReporterContext.js +24 -0
  30. package/lib/commonjs/context/BugReporterContext.js.map +1 -0
  31. package/lib/commonjs/hooks/useScreenshotDetector.js +22 -0
  32. package/lib/commonjs/hooks/useScreenshotDetector.js.map +1 -0
  33. package/lib/commonjs/index.js +87 -0
  34. package/lib/commonjs/index.js.map +1 -0
  35. package/lib/commonjs/native/ScreenshotDetector.js +72 -0
  36. package/lib/commonjs/native/ScreenshotDetector.js.map +1 -0
  37. package/lib/commonjs/navigation/screenTracker.js +47 -0
  38. package/lib/commonjs/navigation/screenTracker.js.map +1 -0
  39. package/lib/commonjs/package.json +1 -0
  40. package/lib/commonjs/services/bugReportService.js +61 -0
  41. package/lib/commonjs/services/bugReportService.js.map +1 -0
  42. package/lib/commonjs/services/supabaseService.js +166 -0
  43. package/lib/commonjs/services/supabaseService.js.map +1 -0
  44. package/lib/commonjs/theme.js +28 -0
  45. package/lib/commonjs/theme.js.map +1 -0
  46. package/lib/commonjs/types.js +35 -0
  47. package/lib/commonjs/types.js.map +1 -0
  48. package/lib/commonjs/utils/logger.js +29 -0
  49. package/lib/commonjs/utils/logger.js.map +1 -0
  50. package/lib/module/BugReporterProvider.js +134 -0
  51. package/lib/module/BugReporterProvider.js.map +1 -0
  52. package/lib/module/collectors/appInfo.js +16 -0
  53. package/lib/module/collectors/appInfo.js.map +1 -0
  54. package/lib/module/collectors/collectContext.js +41 -0
  55. package/lib/module/collectors/collectContext.js.map +1 -0
  56. package/lib/module/collectors/deviceInfo.js +37 -0
  57. package/lib/module/collectors/deviceInfo.js.map +1 -0
  58. package/lib/module/collectors/networkInfo.js +29 -0
  59. package/lib/module/collectors/networkInfo.js.map +1 -0
  60. package/lib/module/components/BugReportAdminScreen.js +221 -0
  61. package/lib/module/components/BugReportAdminScreen.js.map +1 -0
  62. package/lib/module/components/BugReportModal.js +337 -0
  63. package/lib/module/components/BugReportModal.js.map +1 -0
  64. package/lib/module/components/ScreenshotEditor.js +461 -0
  65. package/lib/module/components/ScreenshotEditor.js.map +1 -0
  66. package/lib/module/components/ScreenshotPreview.js +130 -0
  67. package/lib/module/components/ScreenshotPreview.js.map +1 -0
  68. package/lib/module/components/SeveritySelector.js +61 -0
  69. package/lib/module/components/SeveritySelector.js.map +1 -0
  70. package/lib/module/context/BugReporterContext.js +19 -0
  71. package/lib/module/context/BugReporterContext.js.map +1 -0
  72. package/lib/module/hooks/useScreenshotDetector.js +18 -0
  73. package/lib/module/hooks/useScreenshotDetector.js.map +1 -0
  74. package/lib/module/index.js +32 -0
  75. package/lib/module/index.js.map +1 -0
  76. package/lib/module/native/ScreenshotDetector.js +68 -0
  77. package/lib/module/native/ScreenshotDetector.js.map +1 -0
  78. package/lib/module/navigation/screenTracker.js +41 -0
  79. package/lib/module/navigation/screenTracker.js.map +1 -0
  80. package/lib/module/services/bugReportService.js +57 -0
  81. package/lib/module/services/bugReportService.js.map +1 -0
  82. package/lib/module/services/supabaseService.js +159 -0
  83. package/lib/module/services/supabaseService.js.map +1 -0
  84. package/lib/module/theme.js +23 -0
  85. package/lib/module/theme.js.map +1 -0
  86. package/lib/module/types.js +31 -0
  87. package/lib/module/types.js.map +1 -0
  88. package/lib/module/utils/logger.js +25 -0
  89. package/lib/module/utils/logger.js.map +1 -0
  90. package/lib/typescript/src/BugReporterProvider.d.ts +18 -0
  91. package/lib/typescript/src/BugReporterProvider.d.ts.map +1 -0
  92. package/lib/typescript/src/collectors/appInfo.d.ts +6 -0
  93. package/lib/typescript/src/collectors/appInfo.d.ts.map +1 -0
  94. package/lib/typescript/src/collectors/collectContext.d.ts +7 -0
  95. package/lib/typescript/src/collectors/collectContext.d.ts.map +1 -0
  96. package/lib/typescript/src/collectors/deviceInfo.d.ts +7 -0
  97. package/lib/typescript/src/collectors/deviceInfo.d.ts.map +1 -0
  98. package/lib/typescript/src/collectors/networkInfo.d.ts +6 -0
  99. package/lib/typescript/src/collectors/networkInfo.d.ts.map +1 -0
  100. package/lib/typescript/src/components/BugReportAdminScreen.d.ts +11 -0
  101. package/lib/typescript/src/components/BugReportAdminScreen.d.ts.map +1 -0
  102. package/lib/typescript/src/components/BugReportModal.d.ts +20 -0
  103. package/lib/typescript/src/components/BugReportModal.d.ts.map +1 -0
  104. package/lib/typescript/src/components/ScreenshotEditor.d.ts +16 -0
  105. package/lib/typescript/src/components/ScreenshotEditor.d.ts.map +1 -0
  106. package/lib/typescript/src/components/ScreenshotPreview.d.ts +11 -0
  107. package/lib/typescript/src/components/ScreenshotPreview.d.ts.map +1 -0
  108. package/lib/typescript/src/components/SeveritySelector.d.ts +10 -0
  109. package/lib/typescript/src/components/SeveritySelector.d.ts.map +1 -0
  110. package/lib/typescript/src/context/BugReporterContext.d.ts +20 -0
  111. package/lib/typescript/src/context/BugReporterContext.d.ts.map +1 -0
  112. package/lib/typescript/src/hooks/useScreenshotDetector.d.ts +7 -0
  113. package/lib/typescript/src/hooks/useScreenshotDetector.d.ts.map +1 -0
  114. package/lib/typescript/src/index.d.ts +26 -0
  115. package/lib/typescript/src/index.d.ts.map +1 -0
  116. package/lib/typescript/src/native/ScreenshotDetector.d.ts +15 -0
  117. package/lib/typescript/src/native/ScreenshotDetector.d.ts.map +1 -0
  118. package/lib/typescript/src/navigation/screenTracker.d.ts +7 -0
  119. package/lib/typescript/src/navigation/screenTracker.d.ts.map +1 -0
  120. package/lib/typescript/src/services/bugReportService.d.ts +17 -0
  121. package/lib/typescript/src/services/bugReportService.d.ts.map +1 -0
  122. package/lib/typescript/src/services/supabaseService.d.ts +38 -0
  123. package/lib/typescript/src/services/supabaseService.d.ts.map +1 -0
  124. package/lib/typescript/src/theme.d.ts +7 -0
  125. package/lib/typescript/src/theme.d.ts.map +1 -0
  126. package/lib/typescript/src/types.d.ts +144 -0
  127. package/lib/typescript/src/types.d.ts.map +1 -0
  128. package/lib/typescript/src/utils/logger.d.ts +7 -0
  129. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  130. package/package.json +100 -0
  131. package/react-native-bug-reporter.podspec +22 -0
  132. package/react-native.config.js +18 -0
  133. package/src/BugReporterProvider.tsx +178 -0
  134. package/src/collectors/appInfo.ts +15 -0
  135. package/src/collectors/collectContext.ts +47 -0
  136. package/src/collectors/deviceInfo.ts +51 -0
  137. package/src/collectors/networkInfo.ts +31 -0
  138. package/src/components/BugReportAdminScreen.tsx +160 -0
  139. package/src/components/BugReportModal.tsx +315 -0
  140. package/src/components/ScreenshotEditor.tsx +410 -0
  141. package/src/components/ScreenshotPreview.tsx +98 -0
  142. package/src/components/SeveritySelector.tsx +59 -0
  143. package/src/context/BugReporterContext.ts +29 -0
  144. package/src/hooks/useScreenshotDetector.ts +20 -0
  145. package/src/index.ts +51 -0
  146. package/src/native/ScreenshotDetector.ts +87 -0
  147. package/src/navigation/screenTracker.ts +40 -0
  148. package/src/services/bugReportService.ts +81 -0
  149. package/src/services/supabaseService.ts +195 -0
  150. package/src/theme.ts +23 -0
  151. package/src/types.ts +156 -0
  152. package/src/utils/logger.ts +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # react-native-bug-reporter
2
+
3
+ > Universal bug reporting for any React Native app. Detect screenshots, annotate
4
+ > them, collect device/app/user context, and ship reports to **Supabase**
5
+ > (Postgres + Storage). A Database Webhook → Edge Function emails each report.
6
+
7
+ When a user takes a screenshot anywhere in your app, a report modal pops up with
8
+ the screenshot pre-attached. The user can **mark the bug** (pen/arrow/point/text),
9
+ adds a title/description/severity; the SDK auto-collects **user, device, app,
10
+ current screen, network, timestamp** and writes everything to Supabase.
11
+
12
+ - 📸 Automatic screenshot detection (native iOS + Android)
13
+ - ✏️ Built-in screenshot annotation editor (pen, arrow, point, text)
14
+ - 🧩 One-component install — `<BugReporterProvider>`
15
+ - 🔍 Auto-collected context (device, app, user, screen, network, time)
16
+ - 🟢 Supabase backend (Postgres + Storage + Edge Function email) — just URL + anon key
17
+ - 🛠 Built-in in-app admin viewer (live realtime)
18
+ - 🎨 Themeable, TypeScript-first, autolinked
19
+
20
+ ---
21
+
22
+ ## 1. Install
23
+
24
+ ```sh
25
+ npm install react-native-bug-reporter \
26
+ @supabase/supabase-js react-native-url-polyfill \
27
+ @react-native-community/netinfo react-native-device-info \
28
+ react-native-svg react-native-view-shot
29
+ cd ios && pod install && cd ..
30
+ ```
31
+
32
+ The native modules (screenshot detection, svg, view-shot, device-info, netinfo)
33
+ need a **native rebuild** after installing. The Supabase client itself is pure
34
+ JS — no native config files, no `google-services.json`.
35
+
36
+ ---
37
+
38
+ ## 2. Set up Supabase
39
+
40
+ Run [`supabase/schema.sql`](../supabase/schema.sql) in your project's SQL editor
41
+ (creates the `bug_reports` table + RLS + the public `bug-reports` storage
42
+ bucket), then deploy the email Edge Function:
43
+
44
+ ```sh
45
+ supabase functions deploy send-bug-email --no-verify-jwt
46
+ supabase secrets set SMTP_HOST=smtp.gmail.com SMTP_PORT=465 SMTP_SECURE=true \
47
+ SMTP_USER=you@gmail.com SMTP_PASS=<app-password> NOTIFY_EMAIL_TO=admin@you.com
48
+ ```
49
+
50
+ Add a **Database Webhook** (Insert on `bug_reports`) → the `send-bug-email`
51
+ function. Full steps: [`supabase/README.md`](../supabase/README.md).
52
+
53
+ ---
54
+
55
+ ## 3. Wrap your app
56
+
57
+ ```tsx
58
+ import { BugReporterProvider } from 'react-native-bug-reporter';
59
+
60
+ export default function App() {
61
+ return (
62
+ <BugReporterProvider
63
+ config={{
64
+ supabaseUrl: 'https://YOURPROJECT.supabase.co',
65
+ supabaseAnonKey: 'eyJ...anon-key', // safe to ship; RLS controls access
66
+ notifyEmails: 'admin@yourcompany.com', // who gets the email
67
+ environment: __DEV__ ? 'development' : 'production',
68
+ getUser: () => ({ id: user.id, name: user.name, email: user.email }),
69
+ }}
70
+ >
71
+ <RootNavigator />
72
+ </BugReporterProvider>
73
+ );
74
+ }
75
+ ```
76
+
77
+ **Now take a screenshot in the running app — the report modal appears with the
78
+ screenshot attached.** 🎉
79
+
80
+ ---
81
+
82
+ ## 4. Full functionality
83
+
84
+ ### Trigger the modal manually
85
+
86
+ ```tsx
87
+ import { useBugReporter } from 'react-native-bug-reporter';
88
+
89
+ function HelpScreen() {
90
+ const { openReporter } = useBugReporter();
91
+ return <Button title="Report a bug" onPress={() => openReporter()} />;
92
+ }
93
+ ```
94
+
95
+ `useBugReporter()` returns:
96
+
97
+ | Field | Type | Description |
98
+ |-----------------------|-----------------------------------------|-------------|
99
+ | `openReporter` | `(screenshot?) => void` | Open the modal (optionally pre-attach an image). |
100
+ | `closeReporter` | `() => void` | Close it. |
101
+ | `isOpen` | `boolean` | Modal visibility. |
102
+ | `isAutoDetectEnabled` | `boolean` | Whether screenshot detection is on. |
103
+
104
+ ### Track the current screen
105
+
106
+ The SDK is navigation-agnostic. Report the active route so it's attached to bug
107
+ reports. With **React Navigation**:
108
+
109
+ ```tsx
110
+ import { setCurrentScreen, getActiveRouteName } from 'react-native-bug-reporter';
111
+
112
+ <NavigationContainer
113
+ onStateChange={(state) => {
114
+ const route = getActiveRouteName(state);
115
+ if (route) setCurrentScreen(route);
116
+ }}
117
+ />
118
+ ```
119
+
120
+ …then pass `getCurrentScreen` in the config:
121
+
122
+ ```tsx
123
+ import { getCurrentScreen } from 'react-native-bug-reporter';
124
+ // config: { ..., getCurrentScreen }
125
+ ```
126
+
127
+ Or just call `setCurrentScreen('Checkout')` from any screen's effect.
128
+
129
+ ### Admin viewer (in-app)
130
+
131
+ ```tsx
132
+ import { BugReportAdminScreen } from 'react-native-bug-reporter';
133
+
134
+ <BugReportAdminScreen config={{ supabaseUrl, supabaseAnonKey }} />
135
+ ```
136
+
137
+ Live-lists all reports from Supabase (realtime) with screenshot thumbnails and a
138
+ tap-to-cycle status control. Pass the same `supabaseUrl` + `supabaseAnonKey`.
139
+
140
+ ### Theming
141
+
142
+ ```tsx
143
+ config={{
144
+ collectionName: 'bug_reports',
145
+ theme: {
146
+ primaryColor: '#7c3aed',
147
+ backgroundColor: '#ffffff',
148
+ textColor: '#111827',
149
+ mutedColor: '#6b7280',
150
+ borderColor: '#e5e7eb',
151
+ errorColor: '#ef4444',
152
+ },
153
+ }}
154
+ ```
155
+
156
+ ### Callbacks & options
157
+
158
+ ```tsx
159
+ config={{
160
+ collectionName: 'bug_reports',
161
+ disableAutoDetect: false, // set true for manual-only reporting
162
+ debug: __DEV__, // verbose logging
163
+ onSubmitted: (report) => analytics.track('bug_reported', { id: report.id }),
164
+ onError: (err) => console.warn(err),
165
+ }}
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Config reference (`BugReporterConfig`)
171
+
172
+ | Option | Type | Default | Description |
173
+ |---------------------|---------------------------------|----------------|-------------|
174
+ | `supabaseUrl` | `string` **(required)** | — | Supabase project URL. |
175
+ | `supabaseAnonKey` | `string` **(required)** | — | Supabase anon (public) key. |
176
+ | `tableName` | `string` | `bug_reports` | Postgres table for reports. |
177
+ | `storageBucket` | `string` | `bug-reports` | Storage bucket for screenshots. |
178
+ | `notifyEmails` | `string \| string[]` | — | Recipient(s) for the email (stored on the row; Edge Function reads it). |
179
+ | `getUser` | `() => UserInfo \| Promise<…>` | — | Current signed-in user. |
180
+ | `getCurrentScreen` | `() => string \| null` | — | Active route name. |
181
+ | `environment` | `string` | — | e.g. `"production"`. |
182
+ | `showContextSummary`| `boolean` | `true` | Show the "Automatically attached" block (data is always collected). |
183
+ | `disableAutoDetect` | `boolean` | `false` | Manual-only reporting. |
184
+ | `theme` | `BugReporterTheme` | light theme | Color overrides. |
185
+ | `onSubmitted` | `(report) => void` | — | Success callback. |
186
+ | `onError` | `(error) => void` | — | Failure callback. |
187
+ | `debug` | `boolean` | `false` | Verbose logging. |
188
+
189
+ ---
190
+
191
+ ## How screenshot capture works
192
+
193
+ Instead of reading the device Photos library (permission-heavy, may miss the
194
+ frame), the native modules **snapshot the current app window** the instant a
195
+ screenshot is detected:
196
+
197
+ - **iOS** — `userDidTakeScreenshotNotification` → render the key window → PNG.
198
+ - **Android 14+** — `Activity.registerScreenCaptureCallback` → `PixelCopy` → PNG.
199
+ - **Android ≤13** — `MediaStore` `ContentObserver` → `PixelCopy` → PNG.
200
+
201
+ The JS layer shows the image in the modal and streams it to your backend as
202
+ `multipart/form-data`.
203
+
204
+ ---
205
+
206
+ ## Publishing your own copy
207
+
208
+ ```sh
209
+ npm version patch
210
+ npm publish # runs `bob build` via the prepare script
211
+ ```
212
+
213
+ The build emits CommonJS, ESM, and TypeScript declarations to `lib/`.
214
+
215
+ ## License
216
+
217
+ MIT
@@ -0,0 +1,49 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath "com.android.tools.build:gradle:8.1.1"
11
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.24')}"
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "kotlin-android"
17
+
18
+ android {
19
+ namespace "com.bugreporter"
20
+ compileSdkVersion safeExtGet('compileSdkVersion', 35)
21
+
22
+ defaultConfig {
23
+ minSdkVersion safeExtGet('minSdkVersion', 24)
24
+ targetSdkVersion safeExtGet('targetSdkVersion', 35)
25
+ }
26
+
27
+ buildFeatures {
28
+ buildConfig true
29
+ }
30
+
31
+ compileOptions {
32
+ sourceCompatibility JavaVersion.VERSION_17
33
+ targetCompatibility JavaVersion.VERSION_17
34
+ }
35
+
36
+ kotlinOptions {
37
+ jvmTarget = "17"
38
+ }
39
+ }
40
+
41
+ repositories {
42
+ google()
43
+ mavenCentral()
44
+ }
45
+
46
+ dependencies {
47
+ // Resolved against the host app's React Native version.
48
+ implementation "com.facebook.react:react-android"
49
+ }
@@ -0,0 +1,11 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <!-- Official screenshot detection on Android 14+ (no runtime prompt). -->
4
+ <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
5
+
6
+ <!-- MediaStore fallback for screenshot detection on Android 13 and below. -->
7
+ <uses-permission
8
+ android:name="android.permission.READ_MEDIA_IMAGES"
9
+ android:maxSdkVersion="33" />
10
+
11
+ </manifest>
@@ -0,0 +1,227 @@
1
+ package com.bugreporter
2
+
3
+ import android.app.Activity
4
+ import android.database.ContentObserver
5
+ import android.graphics.Bitmap
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.provider.MediaStore
11
+ import android.view.PixelCopy
12
+ import android.view.View
13
+ import com.facebook.react.bridge.Arguments
14
+ import com.facebook.react.bridge.ReactApplicationContext
15
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
16
+ import com.facebook.react.bridge.ReactMethod
17
+ import com.facebook.react.bridge.WritableMap
18
+ import com.facebook.react.modules.core.DeviceEventManagerModule
19
+ import java.io.File
20
+ import java.io.FileOutputStream
21
+ import java.util.concurrent.Executor
22
+
23
+ /**
24
+ * Native screenshot detector for Android.
25
+ *
26
+ * Detection strategy:
27
+ * - API 34+ (Android 14): the official `Activity.registerScreenCaptureCallback`,
28
+ * which fires when the user takes a screenshot — no storage permission needed.
29
+ * - API < 34: a `ContentObserver` on the MediaStore images table that fires when
30
+ * a new image lands in a "Screenshots" path. Requires media read permission.
31
+ *
32
+ * On detection we snapshot the current Activity window (PixelCopy on API 26+,
33
+ * canvas draw otherwise) into a PNG in the cache dir and emit a
34
+ * `BugReporterScreenshotTaken` event with the file URI and dimensions.
35
+ */
36
+ class ScreenshotDetectorModule(private val reactContext: ReactApplicationContext) :
37
+ ReactContextBaseJavaModule(reactContext) {
38
+
39
+ private val mainHandler = Handler(Looper.getMainLooper())
40
+ private var contentObserver: ContentObserver? = null
41
+ private var screenCaptureCallback: Any? = null
42
+ private var isListening = false
43
+
44
+ override fun getName(): String = NAME
45
+
46
+ @ReactMethod
47
+ fun start() {
48
+ if (isListening) return
49
+ isListening = true
50
+ if (Build.VERSION.SDK_INT >= 34) {
51
+ registerScreenCaptureCallback()
52
+ } else {
53
+ registerContentObserver()
54
+ }
55
+ }
56
+
57
+ @ReactMethod
58
+ fun stop() {
59
+ isListening = false
60
+ contentObserver?.let {
61
+ reactContext.contentResolver.unregisterContentObserver(it)
62
+ contentObserver = null
63
+ }
64
+ if (Build.VERSION.SDK_INT >= 34) {
65
+ unregisterScreenCaptureCallback()
66
+ }
67
+ }
68
+
69
+ // Required for NativeEventEmitter on the JS side.
70
+ @ReactMethod
71
+ fun addListener(eventName: String) {}
72
+
73
+ @ReactMethod
74
+ fun removeListeners(count: Int) {}
75
+
76
+ // -- Android 14+ official callback ----------------------------------------
77
+
78
+ private fun registerScreenCaptureCallback() {
79
+ val activity = reactContext.currentActivity ?: return
80
+ mainHandler.post {
81
+ val executor = Executor { command -> mainHandler.post(command) }
82
+ val callback = Activity.ScreenCaptureCallback {
83
+ onScreenshotDetected()
84
+ }
85
+ screenCaptureCallback = callback
86
+ try {
87
+ activity.registerScreenCaptureCallback(executor, callback)
88
+ } catch (e: Exception) {
89
+ // Fall back to the content observer if the callback can't be registered.
90
+ registerContentObserver()
91
+ }
92
+ }
93
+ }
94
+
95
+ private fun unregisterScreenCaptureCallback() {
96
+ val activity = reactContext.currentActivity ?: return
97
+ val callback = screenCaptureCallback as? Activity.ScreenCaptureCallback ?: return
98
+ mainHandler.post {
99
+ try {
100
+ activity.unregisterScreenCaptureCallback(callback)
101
+ } catch (_: Exception) {
102
+ }
103
+ screenCaptureCallback = null
104
+ }
105
+ }
106
+
107
+ // -- Legacy MediaStore observer -------------------------------------------
108
+
109
+ private fun registerContentObserver() {
110
+ if (contentObserver != null) return
111
+ val observer = object : ContentObserver(mainHandler) {
112
+ private var lastDetectedMs = 0L
113
+
114
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
115
+ super.onChange(selfChange, uri)
116
+ if (!isScreenshotUri(uri)) return
117
+ // Debounce: MediaStore can fire several times for one screenshot.
118
+ val now = System.currentTimeMillis()
119
+ if (now - lastDetectedMs < 1500) return
120
+ lastDetectedMs = now
121
+ onScreenshotDetected()
122
+ }
123
+ }
124
+ contentObserver = observer
125
+ reactContext.contentResolver.registerContentObserver(
126
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
127
+ true,
128
+ observer
129
+ )
130
+ }
131
+
132
+ private fun isScreenshotUri(uri: Uri?): Boolean {
133
+ if (uri == null) return true // Some OEMs notify on the table root.
134
+ val path = uri.toString().lowercase()
135
+ return path.contains("screenshot") || path.contains("media")
136
+ }
137
+
138
+ // -- Capture & emit --------------------------------------------------------
139
+
140
+ private fun onScreenshotDetected() {
141
+ val activity = reactContext.currentActivity
142
+ val rootView = activity?.window?.decorView?.rootView
143
+ if (activity == null || rootView == null) {
144
+ emit(null, 0, 0)
145
+ return
146
+ }
147
+ captureView(activity, rootView) { bitmap ->
148
+ if (bitmap == null) {
149
+ emit(null, 0, 0)
150
+ return@captureView
151
+ }
152
+ val uri = persistBitmap(bitmap)
153
+ emit(uri, bitmap.width, bitmap.height)
154
+ }
155
+ }
156
+
157
+ private fun captureView(activity: Activity, view: View, onResult: (Bitmap?) -> Unit) {
158
+ if (Build.VERSION.SDK_INT >= 26) {
159
+ try {
160
+ val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
161
+ val location = IntArray(2)
162
+ view.getLocationInWindow(location)
163
+ PixelCopy.request(
164
+ activity.window,
165
+ android.graphics.Rect(
166
+ location[0],
167
+ location[1],
168
+ location[0] + view.width,
169
+ location[1] + view.height
170
+ ),
171
+ bitmap,
172
+ { result ->
173
+ if (result == PixelCopy.SUCCESS) onResult(bitmap) else drawFallback(view, onResult)
174
+ },
175
+ mainHandler
176
+ )
177
+ return
178
+ } catch (e: Exception) {
179
+ // fall through to canvas draw
180
+ }
181
+ }
182
+ drawFallback(view, onResult)
183
+ }
184
+
185
+ private fun drawFallback(view: View, onResult: (Bitmap?) -> Unit) {
186
+ try {
187
+ if (view.width <= 0 || view.height <= 0) {
188
+ onResult(null)
189
+ return
190
+ }
191
+ val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
192
+ val canvas = android.graphics.Canvas(bitmap)
193
+ view.draw(canvas)
194
+ onResult(bitmap)
195
+ } catch (e: Exception) {
196
+ onResult(null)
197
+ }
198
+ }
199
+
200
+ private fun persistBitmap(bitmap: Bitmap): String? {
201
+ return try {
202
+ val fileName = "bugreport_${System.currentTimeMillis()}.png"
203
+ val file = File(reactContext.cacheDir, fileName)
204
+ FileOutputStream(file).use { out ->
205
+ bitmap.compress(Bitmap.CompressFormat.PNG, 90, out)
206
+ }
207
+ Uri.fromFile(file).toString()
208
+ } catch (e: Exception) {
209
+ null
210
+ }
211
+ }
212
+
213
+ private fun emit(uri: String?, width: Int, height: Int) {
214
+ val params: WritableMap = Arguments.createMap().apply {
215
+ if (uri == null) putNull("uri") else putString("uri", uri)
216
+ putInt("width", width)
217
+ putInt("height", height)
218
+ }
219
+ reactContext
220
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
221
+ .emit("BugReporterScreenshotTaken", params)
222
+ }
223
+
224
+ companion object {
225
+ const val NAME = "RNBugReporterScreenshot"
226
+ }
227
+ }
@@ -0,0 +1,20 @@
1
+ package com.bugreporter
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
+ /**
9
+ * Registers the screenshot detector native module. Add this to the package list
10
+ * in MainApplication.kt.
11
+ */
12
+ class ScreenshotDetectorPackage : ReactPackage {
13
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
14
+ return listOf(ScreenshotDetectorModule(reactContext))
15
+ }
16
+
17
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
18
+ return emptyList()
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+
4
+ // Bridges the Swift RNBugReporterScreenshot class into the React Native runtime.
5
+ // Works in both the legacy bridge and bridgeless (new architecture) via interop.
6
+ @interface RCT_EXTERN_MODULE(RNBugReporterScreenshot, RCTEventEmitter)
7
+
8
+ RCT_EXTERN_METHOD(start)
9
+ RCT_EXTERN_METHOD(stop)
10
+
11
+ @end
@@ -0,0 +1,113 @@
1
+ import Foundation
2
+ import UIKit
3
+ import React
4
+
5
+ /**
6
+ * Native screenshot detector for iOS.
7
+ *
8
+ * Listens for `UIApplication.userDidTakeScreenshotNotification` (no special
9
+ * permission required) and, when the user takes a screenshot, snapshots the
10
+ * current key window into a PNG in the temp directory. The JS side receives a
11
+ * `BugReporterScreenshotTaken` event carrying the file URI and dimensions.
12
+ *
13
+ * Snapshotting the app window (instead of reading the Photos library) keeps the
14
+ * SDK permission-free and guarantees we attach exactly what the user saw.
15
+ */
16
+ @objc(RNBugReporterScreenshot)
17
+ class RNBugReporterScreenshot: RCTEventEmitter {
18
+
19
+ private var hasListeners = false
20
+ private var observer: NSObjectProtocol?
21
+
22
+ override init() {
23
+ super.init()
24
+ }
25
+
26
+ deinit {
27
+ if let observer = observer {
28
+ NotificationCenter.default.removeObserver(observer)
29
+ }
30
+ }
31
+
32
+ @objc override static func requiresMainQueueSetup() -> Bool {
33
+ return false
34
+ }
35
+
36
+ override func supportedEvents() -> [String]! {
37
+ return ["BugReporterScreenshotTaken"]
38
+ }
39
+
40
+ override func startObserving() {
41
+ hasListeners = true
42
+ observer = NotificationCenter.default.addObserver(
43
+ forName: UIApplication.userDidTakeScreenshotNotification,
44
+ object: nil,
45
+ queue: .main
46
+ ) { [weak self] _ in
47
+ self?.handleScreenshot()
48
+ }
49
+ }
50
+
51
+ override func stopObserving() {
52
+ hasListeners = false
53
+ if let observer = observer {
54
+ NotificationCenter.default.removeObserver(observer)
55
+ self.observer = nil
56
+ }
57
+ }
58
+
59
+ /// Manually start detection from JS (idempotent — startObserving handles it).
60
+ @objc func start() {
61
+ // No-op: RCTEventEmitter calls startObserving when JS adds a listener.
62
+ }
63
+
64
+ @objc func stop() {
65
+ // No-op: RCTEventEmitter calls stopObserving when JS removes listeners.
66
+ }
67
+
68
+ private func handleScreenshot() {
69
+ guard hasListeners else { return }
70
+
71
+ DispatchQueue.main.async { [weak self] in
72
+ guard let self = self else { return }
73
+ guard let window = self.keyWindow() else {
74
+ self.sendEvent(withName: "BugReporterScreenshotTaken", body: ["uri": NSNull()])
75
+ return
76
+ }
77
+
78
+ let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
79
+ let image = renderer.image { _ in
80
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: false)
81
+ }
82
+
83
+ guard let data = image.pngData() else {
84
+ self.sendEvent(withName: "BugReporterScreenshotTaken", body: ["uri": NSNull()])
85
+ return
86
+ }
87
+
88
+ let fileName = "bugreport_\(Int(Date().timeIntervalSince1970 * 1000)).png"
89
+ let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
90
+
91
+ do {
92
+ try data.write(to: fileURL, options: .atomic)
93
+ self.sendEvent(
94
+ withName: "BugReporterScreenshotTaken",
95
+ body: [
96
+ "uri": fileURL.absoluteString,
97
+ "width": image.size.width * image.scale,
98
+ "height": image.size.height * image.scale,
99
+ ]
100
+ )
101
+ } catch {
102
+ self.sendEvent(withName: "BugReporterScreenshotTaken", body: ["uri": NSNull()])
103
+ }
104
+ }
105
+ }
106
+
107
+ private func keyWindow() -> UIWindow? {
108
+ return UIApplication.shared.connectedScenes
109
+ .compactMap { $0 as? UIWindowScene }
110
+ .flatMap { $0.windows }
111
+ .first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
112
+ }
113
+ }