@sodyo/react-native-sodyo-sdk 5.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,464 @@
1
+ # Android RNSodyoSdk Code Analysis
2
+
3
+ ## Summary
4
+
5
+ Analysis of all Android source files in the `react-native-sodyo-sdk` bridge layer. Found **4 critical**, **5 major**, and **6 minor** issues across 4 Java files and `build.gradle`.
6
+
7
+ ---
8
+
9
+ ## Critical Issues
10
+
11
+ ### 1. NullPointerException — `getCurrentActivity()` Used Without Null Check
12
+
13
+ **File:** `RNSodyoSdkModule.java:235-236, 242-243`
14
+
15
+ ```java
16
+ // start()
17
+ Activity activity = getCurrentActivity();
18
+ activity.startActivityForResult(intent, SODYO_SCANNER_REQUEST_CODE);
19
+
20
+ // close()
21
+ Activity activity = getCurrentActivity();
22
+ activity.finishActivity(SODYO_SCANNER_REQUEST_CODE);
23
+ ```
24
+
25
+ `getCurrentActivity()` returns `null` when the React Native host activity is not in the foreground (e.g., during transitions, after config changes, or when called too early). This crashes with `NullPointerException`.
26
+
27
+ The same pattern appears in `setTroubleshootMode()` (line 323), `setNormalMode()` (line 330), `startTroubleshoot()` (line 316), and `performMarker()` (line 309) — **6 call sites total**.
28
+
29
+ **Severity:** Critical — crash in production when activity is not available.
30
+
31
+ **Fix:** Add null checks to every call site:
32
+
33
+ ```java
34
+ @ReactMethod
35
+ public void start() {
36
+ Log.i(TAG, "start()");
37
+ Activity activity = getCurrentActivity();
38
+ if (activity == null) {
39
+ Log.e(TAG, "start(): current activity is null");
40
+ return;
41
+ }
42
+ Intent intent = new Intent(activity, SodyoScannerActivity.class);
43
+ activity.startActivityForResult(intent, SODYO_SCANNER_REQUEST_CODE);
44
+ }
45
+ ```
46
+
47
+ ---
48
+
49
+ ### 2. `IllegalArgumentException` Crash in `setEnv()` — No Validation
50
+
51
+ **File:** `RNSodyoSdkModule.java:350`
52
+
53
+ ```java
54
+ String value = String.valueOf(SodyoEnv.valueOf(env.trim().toUpperCase()).getValue());
55
+ ```
56
+
57
+ `Enum.valueOf()` throws `IllegalArgumentException` if the input string doesn't match any enum constant. Any JS call like `setEnv("staging")` crashes the app.
58
+
59
+ Additionally, if `env` is `null`, `env.trim()` throws `NullPointerException`.
60
+
61
+ **Severity:** Critical — crash from any unexpected JS input.
62
+
63
+ **Fix:**
64
+
65
+ ```java
66
+ @ReactMethod
67
+ private void setEnv(String env) {
68
+ Log.i(TAG, "setEnv:" + env);
69
+ if (env == null) {
70
+ Log.e(TAG, "setEnv: env is null");
71
+ return;
72
+ }
73
+ try {
74
+ SodyoEnv sodyoEnv = SodyoEnv.valueOf(env.trim().toUpperCase());
75
+ Map<String, String> params = new HashMap<>();
76
+ params.put("webad_env", String.valueOf(sodyoEnv.getValue()));
77
+ params.put("scanner_QR_code_enabled", "false");
78
+ Sodyo.setScannerParams(params);
79
+ } catch (IllegalArgumentException e) {
80
+ Log.e(TAG, "setEnv: unknown env '" + env + "', expected DEV/QA/PROD");
81
+ }
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ### 3. Fragment View is Null After `commitAllowingStateLoss`
88
+
89
+ **File:** `RNSodyoSdkView.java:67-70`
90
+
91
+ ```java
92
+ fragmentTransaction.add(sodyoFragment, TAG_FRAGMENT).commitAllowingStateLoss();
93
+ fragmentManager.executePendingTransactions();
94
+ view.addView(sodyoFragment.getView(), ...);
95
+ ```
96
+
97
+ The fragment is added **without a container ID** (headless fragment). After `commitAllowingStateLoss()` + `executePendingTransactions()`, the fragment's `onCreateView` may not have been called yet, so `sodyoFragment.getView()` can return `null`. Calling `addView(null, ...)` throws `IllegalArgumentException`.
98
+
99
+ Even if the view is non-null, because the fragment is headless (no container), the fragment lifecycle doesn't manage the view's attachment to the layout — leading to lifecycle mismatches.
100
+
101
+ **Severity:** Critical — crash or blank scanner view.
102
+
103
+ **Fix:** Add the fragment to the container by ID instead of headless:
104
+
105
+ ```java
106
+ view.setId(View.generateViewId());
107
+ fragmentTransaction.add(view.getId(), sodyoFragment, TAG_FRAGMENT).commitAllowingStateLoss();
108
+ fragmentManager.executePendingTransactions();
109
+ // Fragment's view is now automatically placed inside `view`
110
+ ```
111
+
112
+ ---
113
+
114
+ ### 4. ActivityEventListener Never Removed — Leak
115
+
116
+ **File:** `RNSodyoSdkModule.java:75`
117
+
118
+ ```java
119
+ this.reactContext.addActivityEventListener(mActivityEventListener);
120
+ ```
121
+
122
+ The listener is added in the constructor but never removed. The `RNSodyoSdkModule` holds a reference to `reactContext`, and `reactContext` holds a reference back via the listener — creating a mutual reference that prevents garbage collection of both.
123
+
124
+ **Severity:** Critical — memory leak of the entire module and React context on reload.
125
+
126
+ **Fix:** Override `onCatalystInstanceDestroy()`:
127
+
128
+ ```java
129
+ @Override
130
+ public void onCatalystInstanceDestroy() {
131
+ super.onCatalystInstanceDestroy();
132
+ reactContext.removeActivityEventListener(mActivityEventListener);
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Major Issues
139
+
140
+ ### 5. Deprecated `android.app.Fragment` API
141
+
142
+ **File:** `RNSodyoSdkView.java:11-12`
143
+
144
+ ```java
145
+ import android.app.Fragment;
146
+ import android.app.FragmentManager;
147
+ ```
148
+
149
+ `android.app.Fragment` was deprecated in API 28 and **removed in API 30+**. The `@SuppressWarnings("deprecation")` annotation hides the warning but doesn't fix the underlying compatibility issue. On newer devices, this code path may fail or behave unpredictably.
150
+
151
+ **Severity:** Major — forward-compatibility risk on newer Android versions.
152
+
153
+ **Fix:** Migrate to AndroidX `androidx.fragment.app.Fragment` and `FragmentManager`. This requires the host app to use `AppCompatActivity`/`FragmentActivity` (standard in modern RN apps):
154
+
155
+ ```java
156
+ import androidx.fragment.app.Fragment;
157
+ import androidx.fragment.app.FragmentManager;
158
+ // ...
159
+ FragmentManager fragmentManager = ((FragmentActivity) currentActivity).getSupportFragmentManager();
160
+ ```
161
+
162
+ ---
163
+
164
+ ### 6. `Sodyo.getInstance()` Called Without Initialization Guard
165
+
166
+ **File:** `RNSodyoSdkModule.java:109-111`
167
+
168
+ ```java
169
+ Sodyo.getInstance().setSodyoScannerCallback(callbackClosure);
170
+ Sodyo.getInstance().setSodyoMarkerContentCallback(callbackClosure);
171
+ Sodyo.getInstance().setSodyoModeCallback(callbackClosure);
172
+ ```
173
+
174
+ These are called inside `onSodyoAppLoadSuccess`, which is safe. However, `setUserInfo()` at line 251 also calls `Sodyo.getInstance()`. If the JS side calls `setUserInfo` before `init` completes, `getInstance()` may return `null` or throw.
175
+
176
+ **Severity:** Major — crash if methods called before initialization.
177
+
178
+ **Fix:** Add initialization guard:
179
+
180
+ ```java
181
+ @ReactMethod
182
+ public void setUserInfo(ReadableMap userInfo) {
183
+ if (!Sodyo.isInitialized()) {
184
+ Log.w(TAG, "setUserInfo: SDK not initialized yet");
185
+ return;
186
+ }
187
+ if (userInfo != null) {
188
+ Sodyo.getInstance().setUserInfo(ConversionUtil.toMap(userInfo));
189
+ }
190
+ }
191
+ ```
192
+
193
+ ---
194
+
195
+ ### 7. `init()` Silently Ignores Re-initialization Callbacks
196
+
197
+ **File:** `RNSodyoSdkModule.java:212-215`
198
+
199
+ ```java
200
+ if (Sodyo.isInitialized()) {
201
+ Log.i(TAG, "init(): already initialized, ignore");
202
+ return; // callbacks never invoked
203
+ }
204
+ ```
205
+
206
+ If the SDK is already initialized, the success/error callbacks passed from JS are silently dropped. The JS `Promise` or callback will never resolve, potentially leaving the app in a waiting state.
207
+
208
+ **Severity:** Major — JS side hangs waiting for callback that never fires.
209
+
210
+ **Fix:** Invoke the success callback immediately if already initialized:
211
+
212
+ ```java
213
+ if (Sodyo.isInitialized()) {
214
+ Log.i(TAG, "init(): already initialized");
215
+ if (successCallback != null) {
216
+ successCallback.invoke();
217
+ }
218
+ return;
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ### 8. Nested Array Overwrites Parent List in `ConversionUtil.toList()`
225
+
226
+ **File:** `ConversionUtil.java:160`
227
+
228
+ ```java
229
+ case Array:
230
+ result = toList(readableArray.getArray(index)); // overwrites entire result!
231
+ break;
232
+ ```
233
+
234
+ When a nested array is encountered, the **entire `result` list** is replaced with the nested array contents. All previously accumulated items are lost.
235
+
236
+ **Severity:** Major — data corruption for any array containing nested arrays.
237
+
238
+ **Fix:**
239
+
240
+ ```java
241
+ case Array:
242
+ result.add(toList(readableArray.getArray(index)));
243
+ break;
244
+ ```
245
+
246
+ ---
247
+
248
+ ### 9. Null Converted to Key/Index String in ConversionUtil
249
+
250
+ **File:** `ConversionUtil.java:43, 139`
251
+
252
+ ```java
253
+ // toObject()
254
+ case Null:
255
+ result = key; // returns the key name as the value
256
+ break;
257
+
258
+ // toList()
259
+ case Null:
260
+ result.add(String.valueOf(index)); // returns "0", "1", etc.
261
+ break;
262
+ ```
263
+
264
+ When a `null` value is encountered, instead of returning `null`, it returns the key name or the string index. This silently corrupts data — a map `{name: null}` becomes `{name: "name"}`.
265
+
266
+ **Severity:** Major — silent data corruption.
267
+
268
+ **Fix:**
269
+
270
+ ```java
271
+ case Null:
272
+ result = null;
273
+ break;
274
+
275
+ // and in toList:
276
+ case Null:
277
+ result.add(null);
278
+ break;
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Minor Issues
284
+
285
+ ### 10. `@ReactMethod` on `private` Method
286
+
287
+ **File:** `RNSodyoSdkModule.java:346`
288
+
289
+ ```java
290
+ @ReactMethod
291
+ private void setEnv(String env) {
292
+ ```
293
+
294
+ `@ReactMethod` requires methods to be `public`. While this may work in some React Native versions due to reflection, it violates the contract and may break in future RN versions or with ProGuard/R8 optimization.
295
+
296
+ **Severity:** Minor — may break silently with build optimizations.
297
+
298
+ **Fix:** Change to `public`.
299
+
300
+ ---
301
+
302
+ ### 11. `SodyoEnv` Enum Values Appear Swapped
303
+
304
+ **File:** `RNSodyoSdkModule.java:39-41`
305
+
306
+ ```java
307
+ public static enum SodyoEnv {
308
+ DEV(3),
309
+ QA(0), // QA = 0?
310
+ PROD(1); // PROD = 1?
311
+ }
312
+ ```
313
+
314
+ Compare to the iOS side (`RNSodyoSdk.m:138`):
315
+
316
+ ```objc
317
+ NSDictionary *envs = @{ @"DEV": @"3", @"QA": @"1", @"PROD": @"0" };
318
+ ```
319
+
320
+ The values are **different across platforms**:
321
+ - iOS: DEV=3, QA=1, PROD=0
322
+ - Android: DEV=3, QA=0, PROD=1
323
+
324
+ This means the same JS call produces different server environments on iOS vs Android.
325
+
326
+ **Severity:** Minor (but potentially dangerous) — platform behavior inconsistency.
327
+
328
+ **Fix:** Align values across platforms. Determine the correct mapping from the Sodyo SDK documentation and make both platforms match.
329
+
330
+ ---
331
+
332
+ ### 12. `sendEvent()` Called Without Listener Check
333
+
334
+ **File:** `RNSodyoSdkModule.java:356-360`
335
+
336
+ ```java
337
+ private void sendEvent(String eventName, @Nullable WritableMap params) {
338
+ this.reactContext
339
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
340
+ .emit(eventName, params);
341
+ }
342
+ ```
343
+
344
+ If the JS module is not loaded yet or the Catalyst instance is destroyed, `getJSModule()` can throw. Events sent during module teardown will crash.
345
+
346
+ **Severity:** Minor — crash during shutdown/hot reload.
347
+
348
+ **Fix:**
349
+
350
+ ```java
351
+ private void sendEvent(String eventName, @Nullable WritableMap params) {
352
+ if (!reactContext.hasActiveReactInstance()) {
353
+ return;
354
+ }
355
+ reactContext
356
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
357
+ .emit(eventName, params);
358
+ }
359
+ ```
360
+
361
+ ---
362
+
363
+ ### 13. Excessive `Log.i()` in Production
364
+
365
+ **File:** All Java files.
366
+
367
+ Every method logs with `Log.i(TAG, ...)`. Android `Log.i` is visible in production logcat and adds overhead.
368
+
369
+ **Severity:** Minor — performance and information disclosure.
370
+
371
+ **Fix:** Use `BuildConfig.DEBUG` guard or use `Log.d()` instead:
372
+
373
+ ```java
374
+ if (BuildConfig.DEBUG) {
375
+ Log.d(TAG, "start()");
376
+ }
377
+ ```
378
+
379
+ ---
380
+
381
+ ### 14. Outdated `build.gradle` Configuration
382
+
383
+ **File:** `build.gradle`
384
+
385
+ ```groovy
386
+ def DEFAULT_COMPILE_SDK_VERSION = 24 // Android 7.0 — from 2016
387
+ def DEFAULT_BUILD_TOOLS_VERSION = "25.0.2" // 2017 vintage
388
+ def DEFAULT_TARGET_SDK_VERSION = 22 // Android 5.1
389
+ classpath 'com.android.tools.build:gradle:1.3.1' // Gradle plugin from 2015
390
+ ```
391
+
392
+ - `compileSdkVersion 24` — misses 8 years of API improvements
393
+ - `targetSdkVersion 22` — Google Play requires minimum 33+ as of 2024
394
+ - `jcenter()` — shut down, only serves cached artifacts
395
+ - `gradle:1.3.1` — ancient plugin, incompatible with modern AGP
396
+
397
+ These defaults are overridden by the host app in most cases, but they cause issues if the host doesn't specify them.
398
+
399
+ **Severity:** Minor (defaults only) — but causes confusion and build issues.
400
+
401
+ **Fix:** Update defaults to modern values:
402
+
403
+ ```groovy
404
+ def DEFAULT_COMPILE_SDK_VERSION = 34
405
+ def DEFAULT_TARGET_SDK_VERSION = 34
406
+ ```
407
+
408
+ Remove `jcenter()` and the `classpath` line (host app provides the plugin).
409
+
410
+ ---
411
+
412
+ ### 15. `toFlatMap()` Assumes All Values Are Strings
413
+
414
+ **File:** `ConversionUtil.java:117`
415
+
416
+ ```java
417
+ result.put(key, readableMap.getString(key));
418
+ ```
419
+
420
+ If the map contains non-string values (numbers, booleans), `getString()` throws `ClassCastException` or returns unexpected results.
421
+
422
+ **Severity:** Minor — crash if non-string values passed to `setScannerParams`.
423
+
424
+ **Fix:** Convert all values to strings:
425
+
426
+ ```java
427
+ ReadableType type = readableMap.getType(key);
428
+ switch (type) {
429
+ case String: result.put(key, readableMap.getString(key)); break;
430
+ case Number: result.put(key, String.valueOf(readableMap.getDouble(key))); break;
431
+ case Boolean: result.put(key, String.valueOf(readableMap.getBoolean(key))); break;
432
+ default: result.put(key, String.valueOf(toObject(readableMap, key))); break;
433
+ }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Cross-Platform Inconsistencies
439
+
440
+ | Feature | iOS | Android | Issue |
441
+ |---------|-----|---------|-------|
442
+ | `init` re-call | Overwrites callbacks silently | Silently drops callbacks | Both broken, differently |
443
+
444
+ ---
445
+
446
+ ## Issue Summary Table
447
+
448
+ | # | Severity | File | Line(s) | Issue |
449
+ |---|----------|------|---------|-------|
450
+ | 1 | Critical | RNSodyoSdkModule.java | 235,242,309,316,323,330 | Null activity — NPE crash |
451
+ | 2 | Critical | RNSodyoSdkModule.java | 350 | `valueOf()` crash on invalid env |
452
+ | 3 | Critical | RNSodyoSdkView.java | 67-70 | Fragment view null — crash |
453
+ | 4 | Critical | RNSodyoSdkModule.java | 75 | ActivityEventListener never removed — leak |
454
+ | 5 | Major | RNSodyoSdkView.java | 11-12 | Deprecated `android.app.Fragment` |
455
+ | 6 | Major | RNSodyoSdkModule.java | 251 | `getInstance()` before init — NPE |
456
+ | 7 | Major | RNSodyoSdkModule.java | 212-215 | Re-init silently drops callbacks |
457
+ | 8 | Major | ConversionUtil.java | 160 | Nested array overwrites parent list |
458
+ | 9 | Major | ConversionUtil.java | 43,139 | Null → key/index string — data corruption |
459
+ | 10 | Minor | RNSodyoSdkModule.java | 346 | `@ReactMethod` on private method |
460
+ | 11 | Minor | RNSodyoSdkModule.java | 39-41 | Env enum values differ from iOS |
461
+ | 12 | Minor | RNSodyoSdkModule.java | 356 | `sendEvent` without active instance check |
462
+ | 13 | Minor | All files | — | Excessive `Log.i()` in production |
463
+ | 14 | Minor | build.gradle | 3-6,15 | Outdated SDK/plugin defaults |
464
+ | 15 | Minor | ConversionUtil.java | 117 | `toFlatMap` assumes all values are strings |
@@ -1,25 +1,12 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
- def DEFAULT_COMPILE_SDK_VERSION = 24
4
- def DEFAULT_BUILD_TOOLS_VERSION = "25.0.2"
5
- def DEFAULT_TARGET_SDK_VERSION = 22
3
+ // Issue #14 fix: update default SDK versions to modern values
4
+ def DEFAULT_COMPILE_SDK_VERSION = 34
5
+ def DEFAULT_TARGET_SDK_VERSION = 34
6
6
  def DEFAULT_GOOGLE_PLAY_SERVICES_VERSION = "+"
7
7
 
8
- buildscript {
9
- repositories {
10
- jcenter()
11
- mavenCentral()
12
- }
13
-
14
- dependencies {
15
- classpath 'com.android.tools.build:gradle:1.3.1'
16
- }
17
- }
18
-
19
-
20
8
  android {
21
9
  compileSdkVersion project.hasProperty('compileSdkVersion') ? project.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
22
- buildToolsVersion project.hasProperty('buildToolsVersion') ? project.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
23
10
 
24
11
  defaultConfig {
25
12
  minSdkVersion 16
@@ -59,4 +46,4 @@ dependencies {
59
46
  transitive = true
60
47
  exclude group: 'com.parse.bolts', module: 'bolts-android'
61
48
  }
62
- }
49
+ }
@@ -39,8 +39,9 @@ public final class ConversionUtil {
39
39
 
40
40
  ReadableType readableType = readableMap.getType(key);
41
41
  switch (readableType) {
42
+ // Issue #9 fix: return null instead of the key name
42
43
  case Null:
43
- result = key;
44
+ result = null;
44
45
  break;
45
46
  case Boolean:
46
47
  result = readableMap.getBoolean(key);
@@ -101,6 +102,7 @@ public final class ConversionUtil {
101
102
  * @param readableMap The ReadableMap to be conveted.
102
103
  * @return A HashMap containing the data that was in the ReadableMap.
103
104
  */
105
+ // Issue #15 fix: handle non-string value types
104
106
  public static Map<String, String> toFlatMap(@Nullable ReadableMap readableMap) {
105
107
  if (readableMap == null) {
106
108
  return null;
@@ -114,7 +116,24 @@ public final class ConversionUtil {
114
116
  Map<String, String> result = new HashMap<>();
115
117
  while (iterator.hasNextKey()) {
116
118
  String key = iterator.nextKey();
117
- result.put(key, readableMap.getString(key));
119
+ ReadableType type = readableMap.getType(key);
120
+ switch (type) {
121
+ case String:
122
+ result.put(key, readableMap.getString(key));
123
+ break;
124
+ case Number:
125
+ result.put(key, String.valueOf(readableMap.getDouble(key)));
126
+ break;
127
+ case Boolean:
128
+ result.put(key, String.valueOf(readableMap.getBoolean(key)));
129
+ break;
130
+ case Null:
131
+ result.put(key, null);
132
+ break;
133
+ default:
134
+ result.put(key, String.valueOf(toObject(readableMap, key)));
135
+ break;
136
+ }
118
137
  }
119
138
 
120
139
  return result;
@@ -135,8 +154,9 @@ public final class ConversionUtil {
135
154
  for (int index = 0; index < readableArray.size(); index++) {
136
155
  ReadableType readableType = readableArray.getType(index);
137
156
  switch (readableType) {
157
+ // Issue #9 fix: add null instead of index string
138
158
  case Null:
139
- result.add(String.valueOf(index));
159
+ result.add(null);
140
160
  break;
141
161
  case Boolean:
142
162
  result.add(readableArray.getBoolean(index));
@@ -156,8 +176,9 @@ public final class ConversionUtil {
156
176
  case Map:
157
177
  result.add(toMap(readableArray.getMap(index)));
158
178
  break;
179
+ // Issue #8 fix: add nested array to result instead of overwriting
159
180
  case Array:
160
- result = toList(readableArray.getArray(index));
181
+ result.add(toList(readableArray.getArray(index)));
161
182
  break;
162
183
  default:
163
184
  throw new IllegalArgumentException("Could not convert object with index: " + index + ".");
@@ -166,4 +187,4 @@ public final class ConversionUtil {
166
187
 
167
188
  return result;
168
189
  }
169
- }
190
+ }