@phucprime/react-native-image-editor 1.0.3 → 1.0.4

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 (33) hide show
  1. package/README.md +51 -31
  2. package/android/build.gradle +5 -1
  3. package/android/src/main/java/ui/photoeditor/RNPhotoEditorModule.java +155 -60
  4. package/android/src/main/java/ui/photoeditor/RNPhotoEditorPackage.java +50 -12
  5. package/android/src/newarch/java/ui/photoeditor/RNPhotoEditorSpec.java +10 -2
  6. package/android/src/oldarch/java/ui/photoeditor/RNPhotoEditorSpec.java +22 -4
  7. package/ios/RNImageEditor.podspec +39 -8
  8. package/ios/RNPhotoEditor.mm +32 -6
  9. package/ios/RNPhotoEditor.swift +178 -95
  10. package/lib/commonjs/NativeRNPhotoEditor.js +41 -0
  11. package/lib/commonjs/NativeRNPhotoEditor.js.map +1 -0
  12. package/lib/commonjs/index.js +142 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/module/NativeRNPhotoEditor.js +40 -0
  16. package/lib/module/NativeRNPhotoEditor.js.map +1 -0
  17. package/lib/module/index.js +140 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/typescript/NativeRNPhotoEditor.d.ts +46 -0
  20. package/lib/typescript/NativeRNPhotoEditor.d.ts.map +1 -0
  21. package/lib/{index.d.ts → typescript/index.d.ts} +44 -34
  22. package/lib/typescript/index.d.ts.map +1 -0
  23. package/package.json +49 -18
  24. package/src/NativeRNPhotoEditor.ts +38 -8
  25. package/src/index.ts +85 -80
  26. package/android/libs/photo-editor-android-original.jar +0 -0
  27. package/lib/NativeRNPhotoEditor.d.ts +0 -14
  28. package/lib/NativeRNPhotoEditor.d.ts.map +0 -1
  29. package/lib/NativeRNPhotoEditor.js +0 -5
  30. package/lib/NativeRNPhotoEditor.js.map +0 -1
  31. package/lib/index.d.ts.map +0 -1
  32. package/lib/index.js +0 -120
  33. package/lib/index.js.map +0 -1
package/README.md CHANGED
@@ -1,34 +1,50 @@
1
1
  <h1 align="center">@phucprime/react-native-image-editor</h1>
2
2
 
3
3
  <p align="center">
4
- Native image editor for React Native — crop, draw, text, stickers, and more.<br/>
5
- Supports <strong>New Architecture</strong> (Fabric + TurboModules) and the classic bridge.
4
+ A high-performance, native image editing library for React Native — crop, draw, text, stickers, and more.<br/>
5
+ Fully supports the <strong>New Architecture</strong> (Fabric + TurboModules) and legacy bridge mode.
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
9
  <a href="https://www.npmjs.com/package/@phucprime/react-native-image-editor"><img src="https://img.shields.io/npm/v/@phucprime/react-native-image-editor.svg?style=flat-square" alt="npm version" /></a>
10
10
  <a href="https://www.npmjs.com/package/@phucprime/react-native-image-editor"><img src="https://img.shields.io/npm/dm/@phucprime/react-native-image-editor.svg?style=flat-square" alt="npm downloads" /></a>
11
11
  <a href="https://github.com/phucprime/react-native-image-editor/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@phucprime/react-native-image-editor.svg?style=flat-square" alt="license" /></a>
12
+ <a href="https://reactnative.dev/docs/the-new-architecture/landing-page"><img src="https://img.shields.io/badge/React%20Native-New%20Architecture-blue?style=flat-square" alt="New Architecture Ready" /></a>
12
13
  </p>
13
14
 
14
15
  ---
15
16
 
16
- | iOS | Android |
17
- | :-: | :-: |
17
+ ## ⚡ Features
18
+
19
+ - ✂️ **Image Cropping**: Precision native cropping UI.
20
+ - 🎨 **Freehand Drawing**: Brush tool with custom color choices.
21
+ - ✍️ **Text Overlays**: Add, position, scale, and rotate text with custom colors.
22
+ - 🎭 **Stickers**: Add customizable sticker overlays.
23
+ - 🚀 **TurboModules Support**: Native performance on React Native New Architecture.
24
+
25
+ ## 📱 Screenshots
26
+
27
+ | iOS | Android |
28
+ | :--------------------------------------: | :------------------------------------------: |
18
29
  | <img src="assets/ios.gif" width="280" /> | <img src="assets/android.gif" width="280" /> |
19
30
 
20
- ## Compatibility
31
+ ---
32
+
33
+ ## 📋 Compatibility
21
34
 
22
- | | Minimum | Tested |
23
- | --- | --- | --- |
24
- | React Native | 0.73 | **0.78.2** |
25
- | iOS | 13.0 | 18 |
26
- | Android | API 24 | API 35 |
27
- | Architecture | Old Arch ✅ | **New Arch ✅** |
35
+ | Platform / Tool | Minimum | Tested |
36
+ | ---------------- | ----------- | --------------- |
37
+ | **React Native** | `>= 0.73` | **`0.78.2`** |
38
+ | **React** | `>= 18.2.0` | **`19.0.0`** |
39
+ | **iOS** | `13.0` | `18.0` |
40
+ | **Android** | API 24 | API 35 |
41
+ | **Architecture** | Old Arch ✅ | **New Arch ✅** |
28
42
 
29
- > Requires **JDK 17** and **Gradle 8.x** for Android builds.
43
+ > **Note:** Requires **JDK 17** and **Gradle 8.x** for Android builds.
30
44
 
31
- ## Installation
45
+ ---
46
+
47
+ ## 📦 Installation
32
48
 
33
49
  ```bash
34
50
  npm install @phucprime/react-native-image-editor
@@ -75,7 +91,7 @@ Add activities to `AndroidManifest.xml`:
75
91
  <activity android:name="com.yalantis.ucrop.UCropActivity" />
76
92
  ```
77
93
 
78
- ## Usage
94
+ ## 🛠️ Usage
79
95
 
80
96
  ### Promise API (recommended)
81
97
 
@@ -98,29 +114,33 @@ ImageEditor.open({
98
114
  });
99
115
  ```
100
116
 
101
- ## Options
117
+ ## ⚙️ Configuration Options
118
+
119
+ | Property | Type | Default | Description |
120
+ | ---------------- | ---------- | --------------- | ------------------------------------------------------------------------------------------------------ |
121
+ | `path` | `string` | **Required** | Local file URI/path of the image to edit. |
122
+ | `colors` | `string[]` | Default palette | Array of hex color strings for drawing and text. |
123
+ | `stickers` | `string[]` | `[]` | List of sticker image names located in native resource folders. |
124
+ | `hiddenControls` | `string[]` | `[]` | Array of controls to hide (`'text'`, `'clear'`, `'draw'`, `'save'`, `'share'`, `'sticker'`, `'crop'`). |
125
+ | `languages` | `object` | English | Object map for localized UI text strings. |
102
126
 
103
- | Property | Type | Description |
104
- | -------- | ---- | ----------- |
105
- | `path` | `string` | Path to the image file |
106
- | `colors` | `string[]` | Hex colors for draw/text palette |
107
- | `stickers` | `string[]` | Sticker image names from native resources |
108
- | `hiddenControls` | `string[]` | Hide: `'text'` `'clear'` `'draw'` `'save'` `'share'` `'sticker'` `'crop'` |
109
- | `languages` | `object` | UI localization strings |
127
+ > **Tip:** Use [react-native-fs](https://github.com/itinance/react-native-fs) or `@react-native-camera-roll/camera-roll` to handle native file paths in sandbox directories before passing them to the editor.
110
128
 
111
- > **Tip:** Use [react-native-fs](https://github.com/itinance/react-native-fs) to copy images into the app sandbox before editing.
129
+ ---
112
130
 
113
- ## Stickers
131
+ ## 🎨 Adding Custom Stickers
114
132
 
115
- - **iOS** — Add images to the Resources folder
116
- - **Android** — Add images to the `drawable` folder
133
+ - **iOS**: Add image assets (`.png`) to your main Xcode Project's **Resources** bundle.
134
+ - **Android**: Add image assets (`.png`) to `android/app/src/main/res/drawable/`.
117
135
 
118
- See the [Example](Example/) project for a working setup.
136
+ ##### Refer to the [Example](Example/) project for a ready-to-run demonstration setup.
119
137
 
120
- ## Credits
138
+ ## 📄 License
121
139
 
122
- Based on [prscX/react-native-photo-editor](https://github.com/prscX/react-native-photo-editor), with photo editor SDKs by [eventtus](https://github.com/eventtus).
140
+ Distributed under the [Apache 2.0 License](LICENSE).
141
+
142
+ ---
123
143
 
124
- ## License
144
+ ## 🙏 Credits
125
145
 
126
- [Apache 2.0](LICENSE)
146
+ Maintained by **[nguyenhoangphucvnm](https://github.com/nguyenhoangphucvnm)**. Based on [prscX/react-native-photo-editor](https://github.com/prscX/react-native-photo-editor), built upon native photo editor SDKs.
@@ -68,7 +68,11 @@ repositories {
68
68
  }
69
69
 
70
70
  dependencies {
71
- implementation 'com.facebook.react:react-native:+'
71
+ // Use compileOnly with the RN 0.71+ artifact name.
72
+ // 'react-native:+' open range risks version skew between codegen output
73
+ // and runtime; compileOnly ensures the host app's own react-android
74
+ // version is used at runtime without pulling in a second copy.
75
+ compileOnly 'com.facebook.react:react-android'
72
76
  implementation files('libs/photo-editor-android.jar')
73
77
  implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1@aar'
74
78
  implementation 'com.google.android.material:material:1.9.0'
@@ -3,105 +3,200 @@ package ui.photoeditor;
3
3
  import android.app.Activity;
4
4
  import android.content.Intent;
5
5
  import android.graphics.Color;
6
+
7
+ import androidx.annotation.NonNull;
8
+ import androidx.annotation.Nullable;
9
+
6
10
  import com.ahmedadeltito.photoeditor.PhotoEditorActivity;
7
11
  import com.ahmedadeltito.photoeditor.TranslationService;
12
+
8
13
  import com.facebook.react.bridge.ActivityEventListener;
9
14
  import com.facebook.react.bridge.BaseActivityEventListener;
10
- import com.facebook.react.bridge.Callback;
15
+ import com.facebook.react.bridge.Promise;
11
16
  import com.facebook.react.bridge.ReactApplicationContext;
12
17
  import com.facebook.react.bridge.ReactMethod;
13
18
  import com.facebook.react.bridge.ReadableArray;
14
19
  import com.facebook.react.bridge.ReadableMap;
20
+
15
21
  import java.util.ArrayList;
16
22
 
23
+ /**
24
+ * Native module that opens a full-screen photo editor Activity and returns the
25
+ * saved image path (or a cancellation rejection) via a {@link Promise}.
26
+ *
27
+ * Promise vs Callback
28
+ * ───────────────────
29
+ * The previous implementation stored two {@code Callback} instances as instance
30
+ * fields and invoked them from {@code onActivityResult}. On New Architecture,
31
+ * {@code Callback} objects are backed by {@code CallbackHolder} references tied
32
+ * to the JS context. If the JS context reloads (Fast Refresh, error recovery)
33
+ * while the editor Activity is open, those stored callbacks become dangling
34
+ * references — invoking them is either a no-op or a crash.
35
+ *
36
+ * {@link Promise} has the same lifetime guarantee but is explicitly designed for
37
+ * single-shot async operations that span multiple native frames/events. It is
38
+ * also the only pattern fully validated by the New Architecture codegen.
39
+ *
40
+ * Thread safety
41
+ * ─────────────
42
+ * {@code mPendingPromise} is written on the JS thread (inside {@code edit()})
43
+ * and read/nulled on the UI thread (inside {@code onActivityResult}). Both
44
+ * accesses are guarded by synchronisation on {@code this}, and the field is
45
+ * marked {@code volatile} so the write is immediately visible across threads.
46
+ */
17
47
  public class RNPhotoEditorModule extends RNPhotoEditorSpec {
18
48
 
49
+ /** Module name as registered in JS — must match TurboModuleRegistry.getEnforcing('RNPhotoEditor'). */
50
+ public static final String NAME = "RNPhotoEditor";
51
+
52
+ /** startActivityForResult request code — arbitrary, just needs to be unique per Activity. */
19
53
  private static final int PHOTO_EDITOR_REQUEST = 1;
20
- private static final String E_PHOTO_EDITOR_CANCELLED = "E_PHOTO_EDITOR_CANCELLED";
21
-
22
- private Callback mDoneCallback;
23
- private Callback mCancelCallback;
24
-
25
- private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
26
- @Override
27
- public void onActivityResult(
28
- Activity activity,
29
- int requestCode,
30
- int resultCode,
31
- Intent intent
32
- ) {
33
- if (requestCode == PHOTO_EDITOR_REQUEST) {
34
- if (mDoneCallback != null) {
54
+
55
+ /** Error code returned when the user cancels the editor. */
56
+ private static final String E_CANCELLED = "CANCELLED";
57
+
58
+ /** The in-flight Promise. Volatile for cross-thread visibility; guarded by synchronized(this). */
59
+ private volatile @Nullable Promise mPendingPromise;
60
+
61
+ // ─── Activity result listener ─────────────────────────────────────────────
62
+
63
+ private final ActivityEventListener mActivityEventListener =
64
+ new BaseActivityEventListener() {
65
+ @Override
66
+ public void onActivityResult(
67
+ Activity activity,
68
+ int requestCode,
69
+ int resultCode,
70
+ @Nullable Intent intent) {
71
+
72
+ if (requestCode != PHOTO_EDITOR_REQUEST) return;
73
+
74
+ Promise promise;
75
+ synchronized (RNPhotoEditorModule.this) {
76
+ promise = mPendingPromise;
77
+ mPendingPromise = null;
78
+ }
79
+
80
+ if (promise == null) return; // JS context already gone — nothing to do
81
+
35
82
  if (resultCode == Activity.RESULT_CANCELED) {
36
- mCancelCallback.invoke(resultCode);
37
- } else {
38
- mDoneCallback.invoke(intent.getExtras().getString("imagePath"));
83
+ promise.reject(E_CANCELLED, "User cancelled the editor");
84
+ return;
85
+ }
86
+
87
+ // RESULT_OK — extract the saved path from the result Intent
88
+ if (intent == null || intent.getExtras() == null) {
89
+ promise.reject("NO_RESULT", "Editor returned no data");
90
+ return;
39
91
  }
92
+
93
+ String imagePath = intent.getExtras().getString("imagePath");
94
+ if (imagePath == null) {
95
+ promise.reject("NO_PATH", "Editor returned null image path");
96
+ return;
97
+ }
98
+
99
+ promise.resolve(imagePath);
40
100
  }
101
+ };
41
102
 
42
- mCancelCallback = null;
43
- mDoneCallback = null;
44
- }
45
- }
46
- };
103
+ // ─── Constructor ──────────────────────────────────────────────────────────
47
104
 
48
105
  public RNPhotoEditorModule(ReactApplicationContext reactContext) {
49
106
  super(reactContext);
50
107
  reactContext.addActivityEventListener(mActivityEventListener);
51
108
  }
52
109
 
110
+ // ─── TurboModule contract ─────────────────────────────────────────────────
111
+
53
112
  @Override
113
+ @NonNull
54
114
  public String getName() {
55
- return "RNPhotoEditor";
115
+ return NAME;
56
116
  }
57
117
 
118
+ // ─── @ReactMethod ─────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Open the photo editor.
122
+ *
123
+ * Codegen maps this to the {@code edit(props: UnsafeObject): Promise<string>}
124
+ * spec defined in {@code NativeRNPhotoEditor.ts}.
125
+ *
126
+ * @param props Editor configuration (path, colors, stickers, hiddenControls, languages).
127
+ * @param promise Resolved with the saved image path; rejected with code
128
+ * {@value E_CANCELLED} when the user dismisses.
129
+ */
58
130
  @ReactMethod
59
- public void Edit(final ReadableMap props, final Callback onDone, final Callback onCancel) {
60
- String path = props.getString("path");
61
- // print all readable map
62
- TranslationService.getInstance().init(props.getMap("languages").toHashMap());
63
-
64
- //Process Stickers
65
- ReadableArray stickers = props.getArray("stickers");
66
- ArrayList<Integer> stickersIntent = new ArrayList<Integer>();
67
-
68
- for (int i = 0; i < stickers.size(); i++) {
69
- int drawableId = getReactApplicationContext()
70
- .getResources()
71
- .getIdentifier(
72
- stickers.getString(i),
73
- "drawable",
74
- getReactApplicationContext().getPackageName()
75
- );
76
-
77
- stickersIntent.add(drawableId);
131
+ public void edit(@NonNull ReadableMap props, @NonNull Promise promise) {
132
+
133
+ // ── Guard: reject immediately if there is already an editor open ──────
134
+ synchronized (this) {
135
+ if (mPendingPromise != null) {
136
+ promise.reject("ALREADY_OPEN", "An editor session is already in progress");
137
+ return;
138
+ }
139
+ mPendingPromise = promise;
140
+ }
141
+
142
+ // ── Guard: need a live Activity to start the editor ───────────────────
143
+ Activity activity = getCurrentActivity();
144
+ if (activity == null) {
145
+ synchronized (this) { mPendingPromise = null; }
146
+ promise.reject("NO_ACTIVITY", "No active Android Activity found");
147
+ return;
78
148
  }
79
149
 
80
- //Process Hidden Controls
81
- ReadableArray hiddenControls = props.getArray("hiddenControls");
82
- ArrayList hiddenControlsIntent = new ArrayList<>();
150
+ // ── Translations ──────────────────────────────────────────────────────
151
+ ReadableMap languages = props.getMap("languages");
152
+ if (languages != null) {
153
+ TranslationService.getInstance().init(languages.toHashMap());
154
+ }
83
155
 
84
- for (int i = 0; i < hiddenControls.size(); i++) {
85
- hiddenControlsIntent.add(hiddenControls.getString(i));
156
+ // ── Stickers: string names → drawable resource IDs ────────────────────
157
+ ReadableArray stickersArr = props.getArray("stickers");
158
+ ArrayList<Integer> stickersIntent = new ArrayList<>();
159
+ if (stickersArr != null) {
160
+ for (int i = 0; i < stickersArr.size(); i++) {
161
+ int id = getReactApplicationContext()
162
+ .getResources()
163
+ .getIdentifier(
164
+ stickersArr.getString(i),
165
+ "drawable",
166
+ getReactApplicationContext().getPackageName());
167
+ stickersIntent.add(id);
168
+ }
86
169
  }
87
170
 
88
- //Process Colors
89
- ReadableArray colors = props.getArray("colors");
90
- ArrayList colorPickerColors = new ArrayList<>();
171
+ // ── Hidden controls ───────────────────────────────────────────────────
172
+ ReadableArray hiddenArr = props.getArray("hiddenControls");
173
+ ArrayList<String> hiddenControlsIntent = new ArrayList<>();
174
+ if (hiddenArr != null) {
175
+ for (int i = 0; i < hiddenArr.size(); i++) {
176
+ hiddenControlsIntent.add(hiddenArr.getString(i));
177
+ }
178
+ }
91
179
 
92
- for (int i = 0; i < colors.size(); i++) {
93
- colorPickerColors.add(Color.parseColor(colors.getString(i)));
180
+ // ── Colors: hex strings → android.graphics.Color ints ────────────────
181
+ ReadableArray colorsArr = props.getArray("colors");
182
+ ArrayList<Integer> colorPickerColors = new ArrayList<>();
183
+ if (colorsArr != null) {
184
+ for (int i = 0; i < colorsArr.size(); i++) {
185
+ try {
186
+ colorPickerColors.add(Color.parseColor(colorsArr.getString(i)));
187
+ } catch (IllegalArgumentException ignored) {
188
+ // Skip invalid color strings rather than crashing
189
+ }
190
+ }
94
191
  }
95
192
 
96
- Intent intent = new Intent(getCurrentActivity(), PhotoEditorActivity.class);
97
- intent.putExtra("selectedImagePath", path);
193
+ // ── Launch editor Activity ─────────────────────────────────────────────
194
+ Intent intent = new Intent(activity, PhotoEditorActivity.class);
195
+ intent.putExtra("selectedImagePath", props.getString("path"));
98
196
  intent.putExtra("colorPickerColors", colorPickerColors);
99
197
  intent.putExtra("hiddenControls", hiddenControlsIntent);
100
198
  intent.putExtra("stickers", stickersIntent);
101
199
 
102
- mCancelCallback = onCancel;
103
- mDoneCallback = onDone;
104
-
105
- getCurrentActivity().startActivityForResult(intent, PHOTO_EDITOR_REQUEST);
200
+ activity.startActivityForResult(intent, PHOTO_EDITOR_REQUEST);
106
201
  }
107
202
  }
@@ -1,23 +1,61 @@
1
-
2
1
  package ui.photoeditor;
3
2
 
4
- import java.util.Arrays;
5
- import java.util.Collections;
6
- import java.util.List;
3
+ import androidx.annotation.NonNull;
7
4
 
8
- import com.facebook.react.ReactPackage;
5
+ import com.facebook.react.TurboReactPackage;
9
6
  import com.facebook.react.bridge.NativeModule;
10
7
  import com.facebook.react.bridge.ReactApplicationContext;
11
- import com.facebook.react.uimanager.ViewManager;
8
+ import com.facebook.react.module.model.ReactModuleInfo;
9
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
10
+
11
+ import java.util.HashMap;
12
+ import java.util.Map;
13
+
14
+ /**
15
+ * ReactPackage registration for RNPhotoEditor.
16
+ *
17
+ * Extends {@link TurboReactPackage} instead of the old {@link com.facebook.react.ReactPackage}.
18
+ *
19
+ * Why TurboReactPackage?
20
+ * ──────────────────────
21
+ * On New Architecture, React Native discovers TurboModules through the
22
+ * {@link TurboReactPackage#getModule} / {@link TurboReactPackage#getReactModuleInfoProvider}
23
+ * contract. A plain {@code ReactPackage} that only overrides
24
+ * {@code createNativeModules()} is never consulted by the TurboModule registry,
25
+ * so {@code TurboModuleRegistry.getEnforcing("RNPhotoEditor")} would throw
26
+ * "No TurboModule found for RNPhotoEditor" at runtime on New Arch.
27
+ *
28
+ * On Old Architecture, {@link TurboReactPackage} delegates back to
29
+ * {@code getModule()} so no separate code path is needed.
30
+ */
31
+ public class RNPhotoEditorPackage extends TurboReactPackage {
12
32
 
13
- public class RNPhotoEditorPackage implements ReactPackage {
14
33
  @Override
15
- public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
16
- return Arrays.<NativeModule>asList(new RNPhotoEditorModule(reactContext));
34
+ public NativeModule getModule(
35
+ @NonNull String name,
36
+ @NonNull ReactApplicationContext reactContext) {
37
+ if (RNPhotoEditorModule.NAME.equals(name)) {
38
+ return new RNPhotoEditorModule(reactContext);
39
+ }
40
+ return null;
17
41
  }
18
42
 
19
43
  @Override
20
- public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
21
- return Collections.emptyList();
44
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
45
+ return () -> {
46
+ Map<String, ReactModuleInfo> map = new HashMap<>();
47
+ map.put(
48
+ RNPhotoEditorModule.NAME,
49
+ new ReactModuleInfo(
50
+ RNPhotoEditorModule.NAME, // name
51
+ RNPhotoEditorModule.NAME, // className (used for logging)
52
+ false, // canOverrideExistingModule
53
+ false, // needsEagerInit
54
+ false, // isCxxModule
55
+ true // isTurboModule — registers with the JSI TurboModule registry
56
+ )
57
+ );
58
+ return map;
59
+ };
22
60
  }
23
- }
61
+ }
@@ -4,8 +4,16 @@ import com.facebook.react.bridge.ReactApplicationContext;
4
4
 
5
5
  /**
6
6
  * New Architecture spec wrapper.
7
- * On New Architecture, this extends the codegen-generated NativeRNPhotoEditorSpec
8
- * which provides TurboModule integration with compile-time type validation.
7
+ *
8
+ * On New Architecture, the React Native codegen generates
9
+ * {@code NativeRNPhotoEditorSpec} from {@code src/NativeRNPhotoEditor.ts}.
10
+ * That generated class already extends {@code ReactContextBaseJavaModule} and
11
+ * declares the abstract {@code edit(ReadableMap props, Promise promise)} method.
12
+ *
13
+ * This thin wrapper simply inherits from the generated spec so that
14
+ * {@link ui.photoeditor.RNPhotoEditorModule} can extend a single
15
+ * {@code RNPhotoEditorSpec} type regardless of architecture, keeping
16
+ * {@code RNPhotoEditorModule.java} in the shared {@code main} source set.
9
17
  */
10
18
  abstract class RNPhotoEditorSpec extends NativeRNPhotoEditorSpec {
11
19
  RNPhotoEditorSpec(ReactApplicationContext context) {
@@ -1,19 +1,37 @@
1
1
  package ui.photoeditor;
2
2
 
3
- import com.facebook.react.bridge.Callback;
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.bridge.Promise;
4
6
  import com.facebook.react.bridge.ReactApplicationContext;
5
7
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
6
8
  import com.facebook.react.bridge.ReadableMap;
7
9
 
8
10
  /**
9
11
  * Old Architecture compatibility spec.
10
- * On Old Architecture (bridge mode), this extends ReactContextBaseJavaModule directly.
11
- * The concrete module (RNPhotoEditorModule) extends this class.
12
+ *
13
+ * On Old Architecture (classic bridge), there is no codegen-generated base
14
+ * class, so we hand-write the abstract contract here.
15
+ *
16
+ * The method signature mirrors the New Architecture codegen output exactly:
17
+ * {@code edit(ReadableMap props, Promise promise)}
18
+ *
19
+ * This means {@link ui.photoeditor.RNPhotoEditorModule} compiles and behaves
20
+ * identically regardless of which source set ({@code newarch} / {@code oldarch})
21
+ * is active.
12
22
  */
13
23
  abstract class RNPhotoEditorSpec extends ReactContextBaseJavaModule {
24
+
14
25
  RNPhotoEditorSpec(ReactApplicationContext context) {
15
26
  super(context);
16
27
  }
17
28
 
18
- public abstract void Edit(ReadableMap props, Callback onDone, Callback onCancel);
29
+ /**
30
+ * Open the photo editor.
31
+ *
32
+ * @param props Editor configuration map (path, colors, stickers, …).
33
+ * @param promise Resolved with the saved image path on success; rejected
34
+ * with code {@code "CANCELLED"} when the user dismisses.
35
+ */
36
+ public abstract void edit(@NonNull ReadableMap props, @NonNull Promise promise);
19
37
  }
@@ -3,24 +3,55 @@ require "json"
3
3
  package = JSON.parse(File.read(File.join(__dir__, "../package.json")))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = "RNImageEditor"
7
- s.version = package["version"]
8
- s.summary = package["description"]
6
+ s.name = "RNImageEditor"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
9
  s.description = package["description"]
10
- s.homepage = package["homepage"]
11
- s.license = package["license"]
12
- s.author = package["author"]
13
- s.source = { :git => "https://github.com/phucprime/react-native-image-editor.git", :tag => s.version }
10
+ s.homepage = package["homepage"]
11
+ s.license = package["license"]
12
+ s.author = package["author"]
13
+ s.source = {
14
+ :git => "https://github.com/nguyenhoangphucvnm/react-native-image-editor.git",
15
+ :tag => s.version
16
+ }
14
17
 
15
18
  s.platforms = { :ios => "13.0" }
16
19
 
17
20
  s.preserve_paths = "LICENSE", "package.json"
18
- s.source_files = "**/*.{h,m,mm}"
21
+
22
+ # Include .swift — previously missing, which meant RNPhotoEditor.swift was
23
+ # never compiled when consumers installed this pod. The ObjC bridge (.mm)
24
+ # references the Swift class via RCT_EXTERN_MODULE, so both must be compiled.
25
+ s.source_files = "**/*.{h,m,mm,swift}"
26
+
27
+ # External photo-editor SDK (Swift framework)
19
28
  s.dependency "iOSPhotoEditor"
20
29
 
30
+ # Always enforce C++17 so the pod target can include C++ standard-library
31
+ # headers (<utility>, <vector>, <chrono>, …) pulled in transitively by
32
+ # React Native's TurboModule / JSI headers, regardless of RN version or
33
+ # architecture. Without this flag the compiler treats .mm files as plain
34
+ # Objective-C and fails with "'utility' file not found".
35
+ s.pod_target_xcconfig = {
36
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
37
+ "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
38
+ }
39
+
40
+ # install_modules_dependencies wires up all New Architecture (TurboModule /
41
+ # Fabric) pod dependencies automatically on RN >= 0.71. On older RN it falls
42
+ # through to the else branch and adds React-Core manually.
21
43
  if respond_to?(:install_modules_dependencies, true)
22
44
  install_modules_dependencies(s)
23
45
  else
24
46
  s.dependency "React-Core"
47
+
48
+ if ENV["RCT_NEW_ARCH_ENABLED"] == "1"
49
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
50
+ s.dependency "React-Codegen"
51
+ s.dependency "RCT-Folly"
52
+ s.dependency "RCTRequired"
53
+ s.dependency "RCTTypeSafety"
54
+ s.dependency "ReactCommon/turbomodule/core"
55
+ end
25
56
  end
26
57
  end
@@ -4,11 +4,37 @@
4
4
  #import <RNPhotoEditorSpec/RNPhotoEditorSpec.h>
5
5
  #endif
6
6
 
7
+ /**
8
+ * Objective-C++ bridge that exposes the Swift `RNPhotoEditor` class to
9
+ * React Native.
10
+ *
11
+ * Method selector change
12
+ * ──────────────────────
13
+ * The TurboModule codegen spec defines `edit(props: UnsafeObject): Promise<string>`.
14
+ * Codegen maps a Promise-returning method to two extra parameters on the native
15
+ * side — a resolve block and a reject block — using the naming convention:
16
+ *
17
+ * edit:(NSDictionary *)props
18
+ * resolve:(RCTPromiseResolveBlock)resolve
19
+ * reject:(RCTPromiseRejectBlock)reject
20
+ *
21
+ * The previous selector `Edit:onDone:onCancel:` used `RCTResponseSenderBlock`
22
+ * which is not recognised by the TurboModule codegen validator and is unsafe
23
+ * across JS context reloads.
24
+ *
25
+ * requiresMainQueueSetup / methodQueue
26
+ * ─────────────────────────────────────
27
+ * Returning YES from +requiresMainQueueSetup ensures the module is initialised
28
+ * on the main thread. The Swift side also overrides methodQueue to return
29
+ * DispatchQueue.main, so UI work happens synchronously without dispatch_async.
30
+ */
7
31
  @interface RCT_EXTERN_MODULE(RNPhotoEditor, NSObject)
8
32
 
9
- RCT_EXTERN_METHOD(Edit:(NSDictionary *)props
10
- onDone:(RCTResponseSenderBlock)onDone
11
- onCancel:(RCTResponseSenderBlock)onCancel)
33
+ RCT_EXTERN_METHOD(
34
+ edit:(NSDictionary *)props
35
+ resolve:(RCTPromiseResolveBlock)resolve
36
+ reject:(RCTPromiseRejectBlock)reject
37
+ )
12
38
 
13
39
  + (BOOL)requiresMainQueueSetup {
14
40
  return YES;
@@ -17,9 +43,9 @@ RCT_EXTERN_METHOD(Edit:(NSDictionary *)props
17
43
  @end
18
44
 
19
45
  #ifdef RCT_NEW_ARCH_ENABLED
20
- // Conform to the codegen-generated protocol for TurboModule support.
21
- // The actual implementation is in RNPhotoEditor.swift; this declaration
22
- // tells the compiler the Swift class satisfies the protocol.
46
+ // Declare that the Swift class satisfies the codegen-generated protocol.
47
+ // The actual implementation is in RNPhotoEditor.swift; this forward
48
+ // declaration makes the compiler verify conformance at build time.
23
49
  @interface RNPhotoEditor () <NativeRNPhotoEditorSpec>
24
50
  @end
25
51
  #endif