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.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/android/build.gradle +49 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/bugreporter/ScreenshotDetectorModule.kt +227 -0
- package/android/src/main/java/com/bugreporter/ScreenshotDetectorPackage.kt +20 -0
- package/ios/RNBugReporterScreenshot.m +11 -0
- package/ios/RNBugReporterScreenshot.swift +113 -0
- package/lib/commonjs/BugReporterProvider.js +139 -0
- package/lib/commonjs/BugReporterProvider.js.map +1 -0
- package/lib/commonjs/collectors/appInfo.js +21 -0
- package/lib/commonjs/collectors/appInfo.js.map +1 -0
- package/lib/commonjs/collectors/collectContext.js +45 -0
- package/lib/commonjs/collectors/collectContext.js.map +1 -0
- package/lib/commonjs/collectors/deviceInfo.js +41 -0
- package/lib/commonjs/collectors/deviceInfo.js.map +1 -0
- package/lib/commonjs/collectors/networkInfo.js +33 -0
- package/lib/commonjs/collectors/networkInfo.js.map +1 -0
- package/lib/commonjs/components/BugReportAdminScreen.js +225 -0
- package/lib/commonjs/components/BugReportAdminScreen.js.map +1 -0
- package/lib/commonjs/components/BugReportModal.js +341 -0
- package/lib/commonjs/components/BugReportModal.js.map +1 -0
- package/lib/commonjs/components/ScreenshotEditor.js +466 -0
- package/lib/commonjs/components/ScreenshotEditor.js.map +1 -0
- package/lib/commonjs/components/ScreenshotPreview.js +134 -0
- package/lib/commonjs/components/ScreenshotPreview.js.map +1 -0
- package/lib/commonjs/components/SeveritySelector.js +65 -0
- package/lib/commonjs/components/SeveritySelector.js.map +1 -0
- package/lib/commonjs/context/BugReporterContext.js +24 -0
- package/lib/commonjs/context/BugReporterContext.js.map +1 -0
- package/lib/commonjs/hooks/useScreenshotDetector.js +22 -0
- package/lib/commonjs/hooks/useScreenshotDetector.js.map +1 -0
- package/lib/commonjs/index.js +87 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/ScreenshotDetector.js +72 -0
- package/lib/commonjs/native/ScreenshotDetector.js.map +1 -0
- package/lib/commonjs/navigation/screenTracker.js +47 -0
- package/lib/commonjs/navigation/screenTracker.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/services/bugReportService.js +61 -0
- package/lib/commonjs/services/bugReportService.js.map +1 -0
- package/lib/commonjs/services/supabaseService.js +166 -0
- package/lib/commonjs/services/supabaseService.js.map +1 -0
- package/lib/commonjs/theme.js +28 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +35 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/logger.js +29 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/module/BugReporterProvider.js +134 -0
- package/lib/module/BugReporterProvider.js.map +1 -0
- package/lib/module/collectors/appInfo.js +16 -0
- package/lib/module/collectors/appInfo.js.map +1 -0
- package/lib/module/collectors/collectContext.js +41 -0
- package/lib/module/collectors/collectContext.js.map +1 -0
- package/lib/module/collectors/deviceInfo.js +37 -0
- package/lib/module/collectors/deviceInfo.js.map +1 -0
- package/lib/module/collectors/networkInfo.js +29 -0
- package/lib/module/collectors/networkInfo.js.map +1 -0
- package/lib/module/components/BugReportAdminScreen.js +221 -0
- package/lib/module/components/BugReportAdminScreen.js.map +1 -0
- package/lib/module/components/BugReportModal.js +337 -0
- package/lib/module/components/BugReportModal.js.map +1 -0
- package/lib/module/components/ScreenshotEditor.js +461 -0
- package/lib/module/components/ScreenshotEditor.js.map +1 -0
- package/lib/module/components/ScreenshotPreview.js +130 -0
- package/lib/module/components/ScreenshotPreview.js.map +1 -0
- package/lib/module/components/SeveritySelector.js +61 -0
- package/lib/module/components/SeveritySelector.js.map +1 -0
- package/lib/module/context/BugReporterContext.js +19 -0
- package/lib/module/context/BugReporterContext.js.map +1 -0
- package/lib/module/hooks/useScreenshotDetector.js +18 -0
- package/lib/module/hooks/useScreenshotDetector.js.map +1 -0
- package/lib/module/index.js +32 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/ScreenshotDetector.js +68 -0
- package/lib/module/native/ScreenshotDetector.js.map +1 -0
- package/lib/module/navigation/screenTracker.js +41 -0
- package/lib/module/navigation/screenTracker.js.map +1 -0
- package/lib/module/services/bugReportService.js +57 -0
- package/lib/module/services/bugReportService.js.map +1 -0
- package/lib/module/services/supabaseService.js +159 -0
- package/lib/module/services/supabaseService.js.map +1 -0
- package/lib/module/theme.js +23 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +31 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/logger.js +25 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/src/BugReporterProvider.d.ts +18 -0
- package/lib/typescript/src/BugReporterProvider.d.ts.map +1 -0
- package/lib/typescript/src/collectors/appInfo.d.ts +6 -0
- package/lib/typescript/src/collectors/appInfo.d.ts.map +1 -0
- package/lib/typescript/src/collectors/collectContext.d.ts +7 -0
- package/lib/typescript/src/collectors/collectContext.d.ts.map +1 -0
- package/lib/typescript/src/collectors/deviceInfo.d.ts +7 -0
- package/lib/typescript/src/collectors/deviceInfo.d.ts.map +1 -0
- package/lib/typescript/src/collectors/networkInfo.d.ts +6 -0
- package/lib/typescript/src/collectors/networkInfo.d.ts.map +1 -0
- package/lib/typescript/src/components/BugReportAdminScreen.d.ts +11 -0
- package/lib/typescript/src/components/BugReportAdminScreen.d.ts.map +1 -0
- package/lib/typescript/src/components/BugReportModal.d.ts +20 -0
- package/lib/typescript/src/components/BugReportModal.d.ts.map +1 -0
- package/lib/typescript/src/components/ScreenshotEditor.d.ts +16 -0
- package/lib/typescript/src/components/ScreenshotEditor.d.ts.map +1 -0
- package/lib/typescript/src/components/ScreenshotPreview.d.ts +11 -0
- package/lib/typescript/src/components/ScreenshotPreview.d.ts.map +1 -0
- package/lib/typescript/src/components/SeveritySelector.d.ts +10 -0
- package/lib/typescript/src/components/SeveritySelector.d.ts.map +1 -0
- package/lib/typescript/src/context/BugReporterContext.d.ts +20 -0
- package/lib/typescript/src/context/BugReporterContext.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useScreenshotDetector.d.ts +7 -0
- package/lib/typescript/src/hooks/useScreenshotDetector.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +26 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native/ScreenshotDetector.d.ts +15 -0
- package/lib/typescript/src/native/ScreenshotDetector.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screenTracker.d.ts +7 -0
- package/lib/typescript/src/navigation/screenTracker.d.ts.map +1 -0
- package/lib/typescript/src/services/bugReportService.d.ts +17 -0
- package/lib/typescript/src/services/bugReportService.d.ts.map +1 -0
- package/lib/typescript/src/services/supabaseService.d.ts +38 -0
- package/lib/typescript/src/services/supabaseService.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +7 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +144 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +7 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/package.json +100 -0
- package/react-native-bug-reporter.podspec +22 -0
- package/react-native.config.js +18 -0
- package/src/BugReporterProvider.tsx +178 -0
- package/src/collectors/appInfo.ts +15 -0
- package/src/collectors/collectContext.ts +47 -0
- package/src/collectors/deviceInfo.ts +51 -0
- package/src/collectors/networkInfo.ts +31 -0
- package/src/components/BugReportAdminScreen.tsx +160 -0
- package/src/components/BugReportModal.tsx +315 -0
- package/src/components/ScreenshotEditor.tsx +410 -0
- package/src/components/ScreenshotPreview.tsx +98 -0
- package/src/components/SeveritySelector.tsx +59 -0
- package/src/context/BugReporterContext.ts +29 -0
- package/src/hooks/useScreenshotDetector.ts +20 -0
- package/src/index.ts +51 -0
- package/src/native/ScreenshotDetector.ts +87 -0
- package/src/navigation/screenTracker.ts +40 -0
- package/src/services/bugReportService.ts +81 -0
- package/src/services/supabaseService.ts +195 -0
- package/src/theme.ts +23 -0
- package/src/types.ts +156 -0
- 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
|
+
}
|