react-native-rn-story-editor 0.2.0 → 0.2.2

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
@@ -31,38 +31,44 @@ npm install react-native-rn-story-editor
31
31
  ```jsx
32
32
  import { useRef, useEffect } from 'react';
33
33
  import { View, StyleSheet, DeviceEventEmitter } from 'react-native';
34
- import { StoryEditorView, StoryEditorCommands } from 'react-native-rn-story-editor';
34
+ import {
35
+ StoryEditorView,
36
+ StoryEditorCommands,
37
+ } from 'react-native-rn-story-editor';
35
38
 
36
39
  export default function App() {
37
40
  const editorRef = useRef(null);
38
41
 
39
42
  useEffect(() => {
40
43
  // Listen for export events
41
- const subscription = DeviceEventEmitter.addListener('onExportImage', (event) => {
42
- console.log('Exported image base64:', event.image);
43
- });
44
+ const subscription = DeviceEventEmitter.addListener(
45
+ 'onExportImage',
46
+ (event) => {
47
+ console.log('Exported image base64:', event.image);
48
+ }
49
+ );
44
50
  return () => subscription.remove();
45
51
  }, []);
46
52
 
47
53
  return (
48
54
  <View style={styles.container}>
49
- <StoryEditorView
55
+ <StoryEditorView
50
56
  ref={editorRef}
51
57
  colorString="#32a852"
52
58
  baseImage="data:image/png;base64,iVBORw0KGgo..."
53
59
  style={styles.editor}
54
60
  />
55
-
61
+
56
62
  {/* Export to gallery or get base64 */}
57
- <Button
58
- title="Export Image"
59
- onPress={() => StoryEditorCommands.exportImage(editorRef.current)}
63
+ <Button
64
+ title="Export Image"
65
+ onPress={() => StoryEditorCommands.exportImage(editorRef.current)}
60
66
  />
61
-
67
+
62
68
  {/* Show text input dialog */}
63
- <Button
64
- title="Add Text"
65
- onPress={() => StoryEditorCommands.showTextInput(editorRef.current)}
69
+ <Button
70
+ title="Add Text"
71
+ onPress={() => StoryEditorCommands.showTextInput(editorRef.current)}
66
72
  />
67
73
  </View>
68
74
  );
@@ -70,27 +76,27 @@ export default function App() {
70
76
 
71
77
  const styles = StyleSheet.create({
72
78
  container: { flex: 1 },
73
- editor: { width: 300, height: 400 }
79
+ editor: { width: 300, height: 400 },
74
80
  });
75
81
  ```
76
82
 
77
83
  ## Props
78
84
 
79
- | Prop | Type | Default | Description |
80
- |------|------|----------|-------------|
81
- | `color` | `number` | `Color.TRANSPARENT` | Numeric color value (e.g., `0xFF32a852`) |
82
- | `colorString` | `string` | `undefined` | Hex color string (e.g., `"#32a852"`) |
83
- | `baseImage` | `string` | `undefined` | Base64 encoded background image (supports data URI format) |
84
- | `addText` | `string` | `undefined` | Add text layer programmatically |
85
- | `addImage` | `string` | `undefined` | Add overlay image from base64 (supports data URI format) |
86
- | `style` | `ViewStyle` | `undefined` | React Native style object |
85
+ | Prop | Type | Default | Description |
86
+ | ------------- | ----------- | ------------------- | ---------------------------------------------------------- |
87
+ | `color` | `number` | `Color.TRANSPARENT` | Numeric color value (e.g., `0xFF32a852`) |
88
+ | `colorString` | `string` | `undefined` | Hex color string (e.g., `"#32a852"`) |
89
+ | `baseImage` | `string` | `undefined` | Base64 encoded background image (supports data URI format) |
90
+ | `addText` | `string` | `undefined` | Add text layer programmatically |
91
+ | `addImage` | `string` | `undefined` | Add overlay image from base64 (supports data URI format) |
92
+ | `style` | `ViewStyle` | `undefined` | React Native style object |
87
93
 
88
94
  ## Commands
89
95
 
90
- | Command | Parameters | Description |
91
- |---------|------------|-------------|
92
- | `exportImage` | `(ref: React.Ref)` | Shows export dialog with gallery save and database options |
93
- | `showTextInput` | `(ref: React.Ref)` | Shows native text input dialog to add text layer |
96
+ | Command | Parameters | Description |
97
+ | --------------- | ------------------ | ---------------------------------------------------------- |
98
+ | `exportImage` | `(ref: React.Ref)` | Shows export dialog with gallery save and database options |
99
+ | `showTextInput` | `(ref: React.Ref)` | Shows native text input dialog to add text layer |
94
100
 
95
101
  ## Events
96
102
 
@@ -123,16 +129,18 @@ When `exportImage` is called, users see a dialog with:
123
129
  ## Testing
124
130
 
125
131
  ### Unit Tests
132
+
126
133
  ```bash
127
134
  npm test
128
135
  ```
129
136
 
130
137
  ### Manual Testing
138
+
131
139
  ```bash
132
140
  # Test on Android
133
141
  npm run example:android
134
142
 
135
- # Test on iOS
143
+ # Test on iOS
136
144
  npm run example:ios
137
145
 
138
146
  # Test different React Native versions
@@ -142,8 +150,9 @@ npm install react-native@0.73
142
150
  ```
143
151
 
144
152
  ### Cross-Platform Verification
153
+
145
154
  - Android: Tested on API 24+ (emulators/devices)
146
- - iOS: Test on iOS 12+ (simulators/devices)
155
+ - iOS: Test on iOS 12+ (simulators/devices)
147
156
  - React Native: Test with multiple versions
148
157
  - TypeScript: All types compile correctly
149
158
  - Props: Color, style, baseImage, addText, addImage
@@ -153,6 +162,7 @@ npm install react-native@0.73
153
162
  ## Version History
154
163
 
155
164
  ### 0.2.0
165
+
156
166
  - Added layer management (text and image layers)
157
167
  - Added drag-and-drop functionality
158
168
  - Added base64 image support for backgrounds and overlays
@@ -162,6 +172,7 @@ npm install react-native@0.73
162
172
  - Improved event system for JavaScript communication
163
173
 
164
174
  ### 0.1.0
175
+
165
176
  - Initial release
166
177
  - Basic view component with color support
167
178
  - Native commands structure
@@ -6,7 +6,12 @@ import com.facebook.react.bridge.ReactMethod
6
6
  import com.facebook.react.modules.core.DeviceEventManagerModule
7
7
  import com.facebook.react.bridge.Arguments
8
8
  import com.facebook.react.bridge.WritableMap
9
+ import android.util.Log
9
10
 
11
+ /**
12
+ * Event emitter for Story Editor
13
+ * Handles both success events and error events
14
+ */
10
15
  class RnStoryEditorEventEmitter(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
11
16
 
12
17
  override fun getName(): String {
@@ -14,19 +19,95 @@ class RnStoryEditorEventEmitter(reactContext: ReactApplicationContext) : ReactCo
14
19
  }
15
20
 
16
21
  companion object {
22
+ private const val TAG = "StoryEditorEvent"
17
23
  private var reactAppContext: ReactApplicationContext? = null
18
24
 
19
25
  fun setReactContext(context: ReactApplicationContext) {
20
26
  reactAppContext = context
27
+ Log.d(TAG, "React context set successfully")
21
28
  }
22
29
 
30
+ /**
31
+ * Emits a success event with base64 image data
32
+ */
23
33
  fun emitEvent(eventName: String, base64: String) {
24
- val params = Arguments.createMap()
25
- params.putString("image", base64)
26
- reactAppContext?.let { ctx ->
27
- ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
28
- .emit(eventName, params)
34
+ try {
35
+ val params = Arguments.createMap().apply {
36
+ putString("base64", base64)
37
+ putNull("error")
38
+ }
39
+
40
+ reactAppContext?.let { ctx ->
41
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
42
+ .emit(eventName, params)
43
+ Log.d(TAG, "Event emitted successfully: $eventName")
44
+ } ?: run {
45
+ Log.e(TAG, "Cannot emit event - React context is null")
46
+ }
47
+ } catch (e: Exception) {
48
+ Log.e(TAG, "Failed to emit event: ${e.message}")
49
+ emitError(StoryEditorError.unknownError("emitEvent", e))
29
50
  }
30
51
  }
52
+
53
+ /**
54
+ * Emits an error event to JavaScript
55
+ */
56
+ fun emitError(error: StoryEditorError) {
57
+ try {
58
+ val params = Arguments.createMap().apply {
59
+ putString("code", error.code)
60
+ putString("message", error.message)
61
+ error.nativeError?.let {
62
+ putString("nativeError", it.message)
63
+ }
64
+ }
65
+
66
+ reactAppContext?.let { ctx ->
67
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
68
+ .emit("onStoryEditorError", params)
69
+ Log.e(TAG, "Error emitted [${error.code}]: ${error.message}")
70
+ } ?: run {
71
+ Log.e(TAG, "Cannot emit error - React context is null. Error: [${error.code}] ${error.message}")
72
+ }
73
+ } catch (e: Exception) {
74
+ Log.e(TAG, "Failed to emit error event: ${e.message}")
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Convenience method to emit export success
80
+ */
81
+ fun emitExportSuccess(base64: String) {
82
+ emitEvent("onExportImage", base64)
83
+ }
84
+
85
+ /**
86
+ * Convenience method to emit export error
87
+ */
88
+ fun emitExportError(error: StoryEditorError) {
89
+ try {
90
+ val params = Arguments.createMap().apply {
91
+ putString("base64", "")
92
+ putString("errorCode", error.code)
93
+ putString("errorMessage", error.message)
94
+ }
95
+
96
+ reactAppContext?.let { ctx ->
97
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
98
+ .emit("onExportImage", params)
99
+ Log.e(TAG, "Export error emitted [${error.code}]: ${error.message}")
100
+ }
101
+ } catch (e: Exception) {
102
+ Log.e(TAG, "Failed to emit export error: ${e.message}")
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if react context is available
108
+ */
109
+ fun hasReactContext(): Boolean {
110
+ return reactAppContext != null
111
+ }
31
112
  }
32
113
  }
@@ -67,10 +67,14 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
67
67
  }
68
68
 
69
69
  fun addImage(base64: String) {
70
- base64?.let {
71
- val bytes = Base64.decode(it, Base64.DEFAULT)
70
+ safeStoryEditorCall("addImage") {
71
+ val bytes = Base64.decode(base64, Base64.DEFAULT)
72
72
  val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
73
73
 
74
+ if (bitmap == null) {
75
+ throw StoryEditorError.bitmapDecodeFailed("Failed to decode base64 to bitmap")
76
+ }
77
+
74
78
  // Scale image to reasonable size (max 200dp width)
75
79
  val maxWidth = 200f * resources.displayMetrics.density
76
80
  val scale = maxWidth / bitmap.width
@@ -85,29 +89,45 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
85
89
 
86
90
  layers.add(Layer.ImageLayer(scaledBitmap, x, y, scaledWidth, scaledHeight))
87
91
  invalidate()
92
+ }.onError { error ->
93
+ RnStoryEditorEventEmitter.emitError(error)
88
94
  }
89
95
  }
90
96
 
91
97
  fun showTextInputDialog() {
92
98
  Log.d("RnStoryEditorView", "showTextInputDialog called")
93
- val editText = EditText(context)
94
- editText.hint = "Enter text here"
95
99
 
96
- AlertDialog.Builder(context)
97
- .setTitle("Add Text Layer")
98
- .setView(editText)
99
- .setPositiveButton("Add") { dialog, _ ->
100
- val inputText = editText.text.toString()
101
- Log.d("RnStoryEditorView", "Input text: $inputText")
102
- if (inputText.isNotEmpty()) {
103
- addText(inputText)
100
+ try {
101
+ val editText = EditText(context)
102
+ editText.hint = "Enter text here"
103
+
104
+ AlertDialog.Builder(context)
105
+ .setTitle("Add Text Layer")
106
+ .setView(editText)
107
+ .setPositiveButton("Add") { dialog, _ ->
108
+ val inputText = editText.text.toString()
109
+ Log.d("RnStoryEditorView", "Input text: $inputText")
110
+ if (inputText.isNotEmpty()) {
111
+ try {
112
+ addText(inputText)
113
+ } catch (e: Exception) {
114
+ RnStoryEditorEventEmitter.emitError(
115
+ StoryEditorError.textInputFailed("Failed to add text: ${e.message}")
116
+ )
117
+ }
118
+ }
119
+ dialog.dismiss()
104
120
  }
105
- dialog.dismiss()
106
- }
107
- .setNegativeButton("Cancel") { dialog, _ ->
108
- dialog.dismiss()
109
- }
110
- .show()
121
+ .setNegativeButton("Cancel") { dialog, _ ->
122
+ dialog.dismiss()
123
+ }
124
+ .show()
125
+ } catch (e: Exception) {
126
+ Log.e("RnStoryEditorView", "Error showing text input dialog: ${e.message}")
127
+ RnStoryEditorEventEmitter.emitError(
128
+ StoryEditorError.textInputFailed("Dialog creation failed: ${e.message}")
129
+ )
130
+ }
111
131
  }
112
132
 
113
133
  fun showImageOptionsDialog(imageLayer: Layer.ImageLayer) {
@@ -261,8 +281,8 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
261
281
  return true
262
282
  }
263
283
 
264
- fun saveImageToGallery(bitmap: Bitmap): Boolean {
265
- return try {
284
+ fun saveImageToGallery(bitmap: Bitmap): StoryEditorResult<String> {
285
+ return safeStoryEditorCall("saveImageToGallery") {
266
286
  val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
267
287
  val filename = "StoryEditor_$timestamp.png"
268
288
 
@@ -276,23 +296,27 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
276
296
  }
277
297
 
278
298
  val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
279
- uri?.let {
280
- val outputStream: OutputStream? = resolver.openOutputStream(it)
281
- outputStream?.use { stream ->
282
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
299
+ ?: throw StoryEditorError.fileSaveFailed("Failed to create MediaStore entry")
300
+
301
+ resolver.openOutputStream(uri)?.use { stream ->
302
+ if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
303
+ throw StoryEditorError.fileSaveFailed("Bitmap compression failed")
283
304
  }
284
- }
305
+ } ?: throw StoryEditorError.fileSaveFailed("Failed to open output stream")
306
+
307
+ uri.toString()
285
308
  } else {
286
309
  // Android 9 and below use File
287
310
  val directory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "StoryEditor")
288
- if (!directory.exists()) {
289
- directory.mkdirs()
311
+ if (!directory.exists() && !directory.mkdirs()) {
312
+ throw StoryEditorError.fileSaveFailed("Failed to create directory")
290
313
  }
291
314
 
292
315
  val imageFile = File(directory, filename)
293
- val outputStream = FileOutputStream(imageFile)
294
- outputStream.use { stream ->
295
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
316
+ FileOutputStream(imageFile).use { stream ->
317
+ if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
318
+ throw StoryEditorError.fileSaveFailed("Bitmap compression failed")
319
+ }
296
320
  }
297
321
 
298
322
  // Add to gallery
@@ -301,83 +325,100 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
301
325
  put(MediaStore.Images.Media.MIME_TYPE, "image/png")
302
326
  }
303
327
  context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
328
+ imageFile.absolutePath
329
+ }.also {
330
+ Log.d("RnStoryEditorView", "Image saved to gallery: $filename")
304
331
  }
305
-
306
- Log.d("RnStoryEditorView", "Image saved to gallery: $filename")
307
- true
308
- } catch (e: Exception) {
309
- Log.e("RnStoryEditorView", "Error saving image: ${e.message}")
310
- false
311
332
  }
312
333
  }
313
334
 
314
335
  // Merge layers and send base64 back to JS
315
336
  fun exportAndSendBase64() {
316
- val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
317
- val canvas = Canvas(bitmap)
318
- draw(canvas)
337
+ safeStoryEditorCall("exportAndSendBase64") {
338
+ if (width <= 0 || height <= 0) {
339
+ throw StoryEditorError.imageExportFailed("View has invalid dimensions: ${width}x${height}")
340
+ }
341
+
342
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
343
+ val canvas = Canvas(bitmap)
344
+ draw(canvas)
319
345
 
320
- val outputStream = ByteArrayOutputStream()
321
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
322
- val byteArray = outputStream.toByteArray()
323
- val base64 = Base64.encodeToString(byteArray, Base64.NO_WRAP)
346
+ val outputStream = ByteArrayOutputStream()
347
+ if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
348
+ throw StoryEditorError.imageExportFailed("Failed to compress bitmap to PNG")
349
+ }
350
+
351
+ val byteArray = outputStream.toByteArray()
352
+ val base64 = Base64.encodeToString(byteArray, Base64.NO_WRAP)
324
353
 
325
- // Show export dialog with download and database options
326
- showExportPreviewDialog(bitmap, base64)
354
+ // Show export dialog with download and database options
355
+ showExportPreviewDialog(bitmap, base64)
356
+ }.onError { error ->
357
+ RnStoryEditorEventEmitter.emitExportError(error)
358
+ }
327
359
  }
328
360
 
329
361
  fun showExportPreviewDialog(bitmap: Bitmap, base64: String) {
330
- val imageView = ImageView(context)
331
- imageView.setImageBitmap(bitmap)
332
- imageView.setPadding(20, 20, 20, 20)
333
-
334
- val infoText = TextView(context)
335
- val sizeKB = base64.length / 1024
336
- val sizeMB = sizeKB / 1024.0
337
- val imageCount = layers.count { it is Layer.ImageLayer }
338
- val textCount = layers.count { it is Layer.TextLayer }
339
- infoText.text = """
340
- Export Information:
341
- • Image Size: ${bitmap.width} x ${bitmap.height}px
342
- • File Size: ${sizeKB} KB (${String.format("%.2f", sizeMB)} MB)
343
- • Base64 Length: ${base64.length} characters
344
- • Format: PNG with Base64 encoding
345
- • Image Layers: $imageCount
346
- • Text Layers: $textCount
347
- • Total Layers: ${layers.size}
362
+ try {
363
+ val imageView = ImageView(context)
364
+ imageView.setImageBitmap(bitmap)
365
+ imageView.setPadding(20, 20, 20, 20)
348
366
 
349
- Choose export option below.
350
- """.trimIndent()
351
- infoText.setPadding(20, 10, 20, 20)
352
- infoText.textSize = 14f
353
-
354
- val scrollView = ScrollView(context)
355
- val layout = android.widget.LinearLayout(context)
356
- layout.orientation = android.widget.LinearLayout.VERTICAL
357
- layout.addView(imageView)
358
- layout.addView(infoText)
359
- scrollView.addView(layout)
360
-
361
- AlertDialog.Builder(context)
362
- .setTitle("Export Options")
363
- .setView(scrollView)
364
- .setPositiveButton("Download to Gallery") { dialog, _ ->
365
- val success = saveImageToGallery(bitmap)
366
- if (success) {
367
- android.widget.Toast.makeText(context, "Image saved to gallery!", android.widget.Toast.LENGTH_SHORT).show()
368
- } else {
369
- android.widget.Toast.makeText(context, "Failed to save image", android.widget.Toast.LENGTH_SHORT).show()
367
+ val infoText = TextView(context)
368
+ val sizeKB = base64.length / 1024
369
+ val sizeMB = sizeKB / 1024.0
370
+ val imageCount = layers.count { it is Layer.ImageLayer }
371
+ val textCount = layers.count { it is Layer.TextLayer }
372
+ infoText.text = """
373
+ Export Information:
374
+ • Image Size: ${bitmap.width} x ${bitmap.height}px
375
+ • File Size: ${sizeKB} KB (${String.format("%.2f", sizeMB)} MB)
376
+ • Base64 Length: ${base64.length} characters
377
+ • Format: PNG with Base64 encoding
378
+ • Image Layers: $imageCount
379
+ • Text Layers: $textCount
380
+ • Total Layers: ${layers.size}
381
+
382
+ Choose export option below.
383
+ """.trimIndent()
384
+ infoText.setPadding(20, 10, 20, 20)
385
+ infoText.textSize = 14f
386
+
387
+ val scrollView = ScrollView(context)
388
+ val layout = android.widget.LinearLayout(context)
389
+ layout.orientation = android.widget.LinearLayout.VERTICAL
390
+ layout.addView(imageView)
391
+ layout.addView(infoText)
392
+ scrollView.addView(layout)
393
+
394
+ AlertDialog.Builder(context)
395
+ .setTitle("Export Options")
396
+ .setView(scrollView)
397
+ .setPositiveButton("Download to Gallery") { dialog, _ ->
398
+ saveImageToGallery(bitmap)
399
+ .onSuccess { path ->
400
+ android.widget.Toast.makeText(context, "Image saved to gallery!", android.widget.Toast.LENGTH_SHORT).show()
401
+ }
402
+ .onError { error ->
403
+ android.widget.Toast.makeText(context, "Failed to save image: ${error.message}", android.widget.Toast.LENGTH_LONG).show()
404
+ RnStoryEditorEventEmitter.emitError(error)
405
+ }
406
+ dialog.dismiss()
370
407
  }
371
- dialog.dismiss()
372
- }
373
- .setNeutralButton("Send to Database") { dialog, _ ->
374
- // Send event to JS with base64
375
- RnStoryEditorEventEmitter.emitEvent("onExportImage", base64)
376
- dialog.dismiss()
377
- }
378
- .setNegativeButton("Cancel") { dialog, _ ->
379
- dialog.dismiss()
380
- }
381
- .show()
408
+ .setNeutralButton("Send to Database") { dialog, _ ->
409
+ // Send event to JS with base64
410
+ RnStoryEditorEventEmitter.emitExportSuccess(base64)
411
+ dialog.dismiss()
412
+ }
413
+ .setNegativeButton("Cancel") { dialog, _ ->
414
+ dialog.dismiss()
415
+ }
416
+ .show()
417
+ } catch (e: Exception) {
418
+ Log.e("RnStoryEditorView", "Error showing export dialog: ${e.message}")
419
+ RnStoryEditorEventEmitter.emitExportError(
420
+ StoryEditorError.imageExportFailed("Failed to show export dialog: ${e.message}", e)
421
+ )
422
+ }
382
423
  }
383
424
  }
@@ -61,10 +61,28 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
61
61
  @ReactProp(name = "baseImage")
62
62
  override fun setBaseImage(view: RnStoryEditorView, base64: String?) {
63
63
  base64?.let {
64
- val cleanData = cleanBase64(it)
65
- val bytes = android.util.Base64.decode(cleanData, android.util.Base64.DEFAULT)
66
- val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
67
- view.setBaseImage(bitmap)
64
+ try {
65
+ val cleanData = cleanBase64(it)
66
+ val bytes = android.util.Base64.decode(cleanData, android.util.Base64.DEFAULT)
67
+ val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
68
+
69
+ if (bitmap == null) {
70
+ RnStoryEditorEventEmitter.emitError(
71
+ StoryEditorError.bitmapDecodeFailed("Decoded bitmap is null, invalid base64 data")
72
+ )
73
+ return
74
+ }
75
+
76
+ view.setBaseImage(bitmap)
77
+ } catch (e: IllegalArgumentException) {
78
+ RnStoryEditorEventEmitter.emitError(StoryEditorError.invalidBase64())
79
+ } catch (e: OutOfMemoryError) {
80
+ RnStoryEditorEventEmitter.emitError(StoryEditorError.memoryError("decode base64", e))
81
+ } catch (e: Exception) {
82
+ RnStoryEditorEventEmitter.emitError(
83
+ StoryEditorError.bitmapDecodeFailed(e.message ?: "Unknown error")
84
+ )
85
+ }
68
86
  }
69
87
  }
70
88
 
@@ -76,8 +94,18 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
76
94
  @ReactProp(name = "addImage")
77
95
  override fun setAddImage(view: RnStoryEditorView, base64: String?) {
78
96
  base64?.let {
79
- val cleanData = cleanBase64(it)
80
- view.addImage(cleanData)
97
+ try {
98
+ val cleanData = cleanBase64(it)
99
+ view.addImage(cleanData)
100
+ } catch (e: IllegalArgumentException) {
101
+ RnStoryEditorEventEmitter.emitError(StoryEditorError.invalidBase64())
102
+ } catch (e: StoryEditorError) {
103
+ RnStoryEditorEventEmitter.emitError(e)
104
+ } catch (e: Exception) {
105
+ RnStoryEditorEventEmitter.emitError(
106
+ StoryEditorError.layerOperationFailed("add image", e.message ?: "Unknown error")
107
+ )
108
+ }
81
109
  }
82
110
  }
83
111
 
@@ -88,8 +116,20 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
88
116
 
89
117
  override fun receiveCommand(view: RnStoryEditorView, commandId: Int, args: ReadableArray?) {
90
118
  when (commandId) {
91
- 1 -> view.exportAndSendBase64()
92
- 2 -> view.showTextInputDialog()
119
+ 1 -> {
120
+ safeStoryEditorCall("exportImage") {
121
+ view.exportAndSendBase64()
122
+ }.onError { error ->
123
+ RnStoryEditorEventEmitter.emitExportError(error)
124
+ }
125
+ }
126
+ 2 -> {
127
+ safeStoryEditorCall("showTextInput") {
128
+ view.showTextInputDialog()
129
+ }.onError { error ->
130
+ RnStoryEditorEventEmitter.emitError(error)
131
+ }
132
+ }
93
133
  }
94
134
  }
95
135