Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ export {
SelfAppBuilder,
bigIntToString,
brutforceSignatureAlgorithmDsc,
buildSMT,
calculateUserIdentifierHash,
findStartPubKeyIndex,
formatEndpoint,
formatMrz,
genAndInitMockPassportData,
genMockIdDoc,
genMockIdDocAndInitDataParsing,
Comment on lines 65 to 66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t export mock ID generators from the public SDK entrypoint

Shipping genMockIdDoc and genMockIdDocAndInitDataParsing on the main index increases misuse risk in production apps and widens the attack surface. Move these to a dedicated testing entrypoint.

-  genMockIdDoc,
-  genMockIdDocAndInitDataParsing,

Outside-of-hunk: create a test-only entrypoint and re-export there:

// common/testing.ts
export {
  genAndInitMockPassportData,
  genMockIdDoc,
  genMockIdDocAndInitDataParsing,
} from './src/utils/index.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export {
  prepareAadhaarDiscloseTestData,
  prepareAadhaarRegisterData,
  prepareAadhaarRegisterTestData,
} from './src/utils/aadhaar/mockData.js';

Consider also moving genAndInitMockPassportData (Line 63) for consistency.

🤖 Prompt for AI Agents
In common/index.ts around lines 64-65, the public SDK incorrectly exports
test-only functions genMockIdDoc and genMockIdDocAndInitDataParsing; remove
these exports from the main index and instead create a new test-only entrypoint
(e.g., common/testing.ts) that re-exports genAndInitMockPassportData,
genMockIdDoc, genMockIdDocAndInitDataParsing from ./src/utils/index.js and any
other test helpers (generateTestData, testCustomData,
prepareAadhaarDiscloseTestData, etc.); update package exports (or build config)
to expose the new testing entrypoint only for tests and adjust any imports in
the repo to point to common/testing where needed, optionally moving
genAndInitMockPassportData (line 63) into the test entrypoint for consistency.

generateCircuitInputsDSC,
generateCircuitInputsRegister,
generateCircuitInputsVCandDisclose,
Expand All @@ -69,28 +73,17 @@ export {
getLeafCscaTree,
getLeafDscTree,
getSKIPEM,
getSolidityPackedUserContextData,
getUniversalLink,
hashEndpointWithScope,
initElliptic,
initPassportDataParsing,
parseCertificateSimple,
parseDscCertificateData,
genMockIdDoc,
genMockIdDocAndInitDataParsing,
buildSMT,
calculateUserIdentifierHash,
getSolidityPackedUserContextData,
stringToBigInt,
} from './src/utils/index.js';

export {
prepareAadhaarRegisterTestData,
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
} from './src/utils/aadhaar/mockData.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export { createSelector } from './src/utils/aadhaar/constants.js';

// Hash utilities
export {
customHasher,
Expand All @@ -99,3 +92,11 @@ export {
hash,
packBytesAndPoseidon,
} from './src/utils/hash.js';

export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';

export {
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
prepareAadhaarRegisterTestData,
} from './src/utils/aadhaar/mockData.js';
5 changes: 5 additions & 0 deletions packages/mobile-sdk-alpha/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ android {
exclude 'META-INF/androidx.exifinterface_exifinterface.version'
pickFirst '**/libc++_shared.so'
pickFirst '**/libjsc.so'
pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid pickFirst on OSGI manifest; exclude instead for deterministic builds.

pickFirst masks conflicts and depends on dependency order. Prefer excluding the OSGI metadata so builds are stable.

-        pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
+        exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
🤖 Prompt for AI Agents
In packages/mobile-sdk-alpha/android/build.gradle around line 99, the build
currently uses pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' which masks
conflicts and yields non-deterministic builds; replace this with an explicit
exclusion in the packagingOptions/resources (or
android.packagingOptions.resources.excludes) to remove that OSGI manifest
instead of picking first so the file is excluded deterministically from the
APK/AAR.

}
}

Expand Down Expand Up @@ -155,4 +156,8 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'

implementation 'com.github.mhshams:jnbis:2.0.2'

// QR Code scanning dependencies
implementation 'com.google.zxing:core:3.5.2'
implementation 'com.google.zxing:android-core:3.3.0'
Comment on lines +160 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Drop zxing:android-core (deprecated, pre‑AndroidX); it can clash with AndroidX and add legacy support libs.

If you’re decoding from CameraX frames, ZXing core is sufficient. If you need camera UI/helpers, use journeyapps’ AndroidX artifact instead.

Minimal change:

-    implementation 'com.google.zxing:core:3.5.2'
-    implementation 'com.google.zxing:android-core:3.3.0'
+    implementation 'com.google.zxing:core:3.5.2'

Alternative (if you want the embedded helpers):

-    implementation 'com.google.zxing:core:3.5.2'
-    implementation 'com.google.zxing:android-core:3.3.0'
+    implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// QR Code scanning dependencies
implementation 'com.google.zxing:core:3.5.2'
implementation 'com.google.zxing:android-core:3.3.0'
// QR Code scanning dependencies
implementation 'com.google.zxing:core:3.5.2'
🤖 Prompt for AI Agents
In packages/mobile-sdk-alpha/android/build.gradle around lines 160 to 162,
remove the deprecated pre-AndroidX dependency
"com.google.zxing:android-core:3.3.0" to avoid AndroidX clashes and legacy
support libs; keep "com.google.zxing:core:3.5.2" for decoding from CameraX
frames, or if you need embedded camera UI/helpers replace the removed line with
the AndroidX-compatible JourneyApps artifact (android-integration) and update
its version accordingly.

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ class RNSelfPassportReaderPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(
RNSelfPassportReaderModule(reactContext),
SelfMRZScannerModule(reactContext)
SelfMRZScannerModule(reactContext),
SelfQRScannerModule(reactContext)
)
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(SelfOCRViewManager(reactContext))
return listOf(
SelfOCRViewManager(reactContext),
SelfQRScannerViewManager(reactContext)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,104 @@ ReactContextBaseJavaModule(reactContext), CameraMLKitFragment.CameraMLKitCallbac
override fun getName() = "SelfMRZScannerModule"

private var scanPromise: Promise? = null
private var currentContainer: FrameLayout? = null
private var currentFragment: CameraMLKitFragment? = null

@ReactMethod
fun startScanning(promise: Promise) {
scanPromise = promise
val activity = reactApplicationContext.currentActivity as? FragmentActivity ?: return

activity.runOnUiThread {
val container = FrameLayout(activity)
val containerId = View.generateViewId()
container.id = containerId

activity.addContentView(container, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
))

activity.supportFragmentManager
.beginTransaction()
.replace(containerId, CameraMLKitFragment(this))
.commit()
val activity = reactApplicationContext.currentActivity as? FragmentActivity
if (currentFragment != null || currentContainer != null || scanPromise != null) {
promise.reject("E_SCAN_IN_PROGRESS", "Scanning already in progress")
return
}
if (activity == null) {
promise.reject("E_NO_ACTIVITY", "No FragmentActivity found")
return
}
scanPromise = promise


activity.runOnUiThread {
try {
val container = FrameLayout(activity)
// just using view.generateViewId() doesn't work.
val containerId = generateUnusedId(activity.window.decorView as ViewGroup)
container.id = containerId

container.layoutParams = ViewGroup.LayoutParams(
Comment on lines +44 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent orphaned overlays on exceptions by storing container early.

If an exception occurs before currentContainer is set, the added view can stick. Set the reference immediately after assigning the ID.

                 val container = FrameLayout(activity)
                 // just using view.generateViewId() doesn't work.
                 val containerId = generateUnusedId(activity.window.decorView as ViewGroup)
                 container.id = containerId
+                // Ensure we can clean up even if later steps fail
+                currentContainer = container
@@
-                // Store references for cleanup
-                currentContainer = container
-                currentFragment = fragment
+                // Store fragment reference for cleanup
+                currentFragment = fragment

Also applies to: 66-68

🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfMRZScannerModule.kt
around lines 44-49 (and also apply the same change at lines ~66-68), the newly
created FrameLayout container is assigned an ID and added to the view hierarchy
but currentContainer isn’t set until later, which can leave an orphaned overlay
if an exception occurs; fix this by assigning currentContainer = container
immediately after setting container.id (before any operations that may throw) so
the reference exists for cleanup, and make the identical change at the other
location mentioned (lines 66-68).

ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)

container.isFocusable = true
container.isFocusableInTouchMode = true
container.setBackgroundColor(android.graphics.Color.BLACK)

activity.addContentView(container, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
))

val fragment = CameraMLKitFragment(this@SelfMRZScannerModule)

// Store references for cleanup
currentContainer = container
currentFragment = fragment

activity.supportFragmentManager
.beginTransaction()
.replace(containerId, fragment)
.commitNow()

Comment on lines +69 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid commitNow crashes when state is saved; guard and fall back.

commitNow() can throw if FragmentManager state is saved. Use isStateSaved and fall back to commitAllowingStateLoss().

-                activity.supportFragmentManager
-                    .beginTransaction()
-                    .replace(containerId, fragment)
-                    .commitNow()
+                val fm = activity.supportFragmentManager
+                val tx = fm.beginTransaction().replace(containerId, fragment)
+                if (fm.isStateSaved) {
+                    tx.commitAllowingStateLoss()
+                    android.util.Log.w("SelfMRZScannerModule", "Fragment committed with state loss due to saved state")
+                } else {
+                    tx.commitNow()
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
activity.supportFragmentManager
.beginTransaction()
.replace(containerId, fragment)
.commitNow()
val fm = activity.supportFragmentManager
val tx = fm.beginTransaction().replace(containerId, fragment)
if (fm.isStateSaved) {
tx.commitAllowingStateLoss()
android.util.Log.w("SelfMRZScannerModule", "Fragment committed with state loss due to saved state")
} else {
tx.commitNow()
}
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfMRZScannerModule.kt
around lines 69 to 73, the code calls fragment transactions with commitNow()
which can throw if the FragmentManager state is already saved; change the logic
to check activity.supportFragmentManager.isStateSaved() before committing and,
if true, use commitAllowingStateLoss() (or commitNowAllowingStateLoss() if
immediate behavior is required) as a safe fallback so the app doesn't crash when
state is saved.

} catch (e: Exception) {
android.util.Log.e("SelfMRZScannerModule", "Error in startScanning", e)
promise.reject("E_SCANNING_ERROR", e.message, e)
cleanup()
}
}
}

override fun onPassportRead(mrzInfo: MRZInfo) {
scanPromise?.resolve(mrzInfo.toString())
scanPromise = null
cleanup()
}
Comment on lines 82 to 86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t return raw MRZ string; minimize/structure PII.

MRZInfo.toString() can expose full passport details to JS/logs. Return a minimal, structured payload with masking.

-        scanPromise?.resolve(mrzInfo.toString())
+        val payload = com.facebook.react.bridge.Arguments.createMap().apply {
+            putString("documentType", mrzInfo.documentCode)
+            putString("issuingState", mrzInfo.issuingState)
+            putString("nationality", mrzInfo.nationality)
+            putString("documentNumberMasked", mrzInfo.documentNumber?.let { maskDoc(it) })
+            // Add more fields only if strictly required; avoid full MRZ dump.
+        }
+        scanPromise?.resolve(payload)

Add alongside the class (helper):

private fun maskDoc(v: String): String =
    if (v.length <= 3) "***" else "*".repeat(v.length - 3) + v.takeLast(3)

If JS currently expects a string, consider a temporary dual-path (behind a debug flag) or bumping the API contract with a clear migration note.


override fun onError(e: Exception) {
scanPromise?.reject(e)
scanPromise = null
cleanup()
}
Comment on lines 53 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use explicit error code for JS contract consistency.

Promise.reject(e) yields unspecified codes; standardize to a named code to allow JS to handle reliably.

-    override fun onError(e: Exception) {
-        scanPromise?.reject(e)
+    override fun onError(e: Exception) {
+        scanPromise?.reject("E_MRZ_SCAN_ERROR", e.message, e)
         scanPromise = null
         cleanup()
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun onError(e: Exception) {
scanPromise?.reject(e)
scanPromise = null
cleanup()
}
override fun onError(e: Exception) {
scanPromise?.reject("E_MRZ_SCAN_ERROR", e.message, e)
scanPromise = null
cleanup()
}
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfMRZScannerModule.kt
around lines 88 to 92, the onError currently calls scanPromise?.reject(e) which
yields unspecified error codes to JS; change it to reject with an explicit
string error code (e.g. "SCAN_ERROR"), pass the exception message as the second
parameter, and include the original exception as the throwable/third parameter
so JS receives a stable code, readable message, and the underlying exception;
keep the subsequent scanPromise = null and cleanup() calls unchanged.


private fun generateUnusedId(root: ViewGroup): Int {
var id: Int
do { id = View.generateViewId() } while (root.findViewById<View>(id) != null)
return id
}

private fun cleanup() {
val activity = reactApplicationContext.currentActivity as? FragmentActivity
val fragment = currentFragment
val container = currentContainer
currentFragment = null
currentContainer = null
scanPromise = null

if (activity != null && fragment != null && container != null) {
activity.runOnUiThread {
try {
activity.supportFragmentManager
.beginTransaction()
.remove(fragment)
.commitAllowingStateLoss()

(container.parent as? ViewGroup)?.removeView(container)

android.util.Log.d("SelfMRZScannerModule", "Cleaned up fragment and container")
} catch (e: Exception) {
android.util.Log.e("SelfMRZScannerModule", "Error during cleanup", e)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ class SelfOCRViewManager(

override fun getName() = REACT_CLASS

override fun createViewInstance(reactContext: ThemedReactContext) =
FrameLayout(reactContext)
override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout {
return FrameLayout(reactContext)
}

override fun getCommandsMap() = mapOf(
"create" to COMMAND_CREATE,
Expand All @@ -43,11 +44,50 @@ class SelfOCRViewManager(
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)

when (commandId.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
COMMAND_DESTROY -> destroyFragment(root, reactNativeViewId)
val reactNativeViewId = args?.getInt(0)
if (reactNativeViewId == null) {
android.util.Log.w("SelfOCRViewManager", "receiveCommand called with null or empty args for command: $commandId")
return
}
Comment on lines +48 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard ReadableArray bounds/types; current access can throw

args?.getInt(0) will still crash if args exists but is empty or type ≠ number. Validate size and type before reading.

Apply:

-        val reactNativeViewId = args?.getInt(0)
-        if (reactNativeViewId == null) {
-            android.util.Log.w("SelfOCRViewManager", "receiveCommand called with null or empty args for command: $commandId")
-            return
-        }
+        val reactNativeViewId = args?.let { arr ->
+            if (arr.size() > 0 && arr.getType(0) == ReadableType.Number) arr.getInt(0) else null
+        }
+        if (reactNativeViewId == null) {
+            android.util.Log.w("SelfOCRViewManager", "receiveCommand: missing/invalid viewId for command=$commandId; args=$args")
+            return
+        }

Add import:

import com.facebook.react.bridge.ReadableType
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfOCRViewManager.kt
around lines 48 to 52, the code accesses args?.getInt(0) without checking bounds
or type which can crash if args exists but is empty or the element is not a
number; update the code to first import com.facebook.react.bridge.ReadableType,
then guard by verifying args != null && args.size() > 0 && args.getType(0) ==
ReadableType.Number before calling getInt(0), and if the checks fail log a
warning with context and return to avoid the crash.


when (commandId) {
"create" -> {
createFragment(root, reactNativeViewId)
}
"destroy" -> {
destroyFragment(root, reactNativeViewId)
}
else -> {
android.util.Log.w("SelfOCRViewManager", "Unknown command: $commandId")
}
}
}

// Alternative method signature for newer React Native versions
override fun receiveCommand(
root: FrameLayout,
commandId: Int,
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)

val reactNativeViewId = args?.getInt(0)
if (reactNativeViewId == null) {
android.util.Log.w("SelfOCRViewManager", "receiveCommand called with null or empty args for command: $commandId")
return
}

when (commandId) {
COMMAND_CREATE -> {
createFragment(root, reactNativeViewId)
}
COMMAND_DESTROY -> {
destroyFragment(root, reactNativeViewId)
}
else -> {
android.util.Log.w("SelfOCRViewManager", "Unknown command: $commandId")
}
}
}

Expand All @@ -57,18 +97,55 @@ class SelfOCRViewManager(
if (index == 1) propHeight = value
}

// @ReactProp(name = "width")
// fun setWidth(view: FrameLayout, width: Int) {
// propWidth = width
// }

// @ReactProp(name = "height")
// fun setHeight(view: FrameLayout, height: Int) {
// propHeight = height
// }

private fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
this.reactNativeViewId = reactNativeViewId
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
if (parentView == null) {
android.util.Log.e("SelfOCRViewManager", "Parent view not found for ID: $reactNativeViewId")
return
}
setupLayout(parentView)

val activity = reactContext.currentActivity as? FragmentActivity
if (activity == null) {
android.util.Log.e("SelfOCRViewManager", "No FragmentActivity found")
return
}

// Check if activity is in a valid state
if (activity.isFinishing || activity.isDestroyed) {
android.util.Log.e("SelfOCRViewManager", "Activity is finishing or destroyed")
return
}

Comment on lines +119 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Null-check container before layout; bail early if missing

root.findViewById may return null; setupLayout(parentView) will then NPE and the subsequent fragment transaction will still run, risking IllegalArgumentException on replace().

-        val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
-        setupLayout(parentView)
+        val parentView = root.findViewById<ViewGroup?>(reactNativeViewId)
+        if (parentView == null) {
+            android.util.Log.e("SelfOCRViewManager", "Container view $reactNativeViewId not found in root")
+            return
+        }
+        setupLayout(parentView)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val activity = reactContext.currentActivity as? FragmentActivity
if (activity == null) {
android.util.Log.e("SelfOCRViewManager", "No FragmentActivity found")
return
}
// Check if activity is in a valid state
if (activity.isFinishing || activity.isDestroyed) {
android.util.Log.e("SelfOCRViewManager", "Activity is finishing or destroyed")
return
}
val activity = reactContext.currentActivity as? FragmentActivity
if (activity == null) {
android.util.Log.e("SelfOCRViewManager", "No FragmentActivity found")
return
}
// Check if activity is in a valid state
if (activity.isFinishing || activity.isDestroyed) {
android.util.Log.e("SelfOCRViewManager", "Activity is finishing or destroyed")
return
}
val parentView = root.findViewById<ViewGroup?>(reactNativeViewId)
if (parentView == null) {
android.util.Log.e("SelfOCRViewManager", "Container view $reactNativeViewId not found in root")
return
}
setupLayout(parentView)
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfOCRViewManager.kt
around lines 105 to 116, root.findViewById can return null so
setupLayout(parentView) may NPE and the fragment transaction could still run;
check the parentView returned by findViewById and if it's null log an error and
return early before calling setupLayout or performing any fragment transaction
so you don't attempt to replace a fragment into a null container.

val cameraFragment = CameraMLKitFragment(this)
// val cameraFragment = MyFragment()
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, cameraFragment, reactNativeViewId.toString())
.commit()
android.util.Log.d("SelfOCRViewManager", "Starting fragment transaction")

// Post to ensure activity is fully ready
activity.window.decorView.post {
try {
if (!activity.isFinishing && !activity.isDestroyed) {
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, cameraFragment, reactNativeViewId.toString())
.commitNow()
} else {
android.util.Log.e("SelfOCRViewManager", "Activity no longer valid for fragment transaction")
}
} catch (e: Exception) {
android.util.Log.e("SelfOCRViewManager", "Fragment transaction failed", e)
}
}
Comment on lines +132 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid commitNow after state save; check isStateSaved and use reordering

commitNow() can throw IllegalStateException if state is saved. Check FragmentManager.isStateSaved and prefer commit() with setReorderingAllowed(true).

-        activity.window.decorView.post {
-            try {
-                if (!activity.isFinishing && !activity.isDestroyed) {
-                    activity.supportFragmentManager
-                        .beginTransaction()
-                        .replace(reactNativeViewId, cameraFragment, reactNativeViewId.toString())
-                        .commitNow()
-                } else {
-                    android.util.Log.e("SelfOCRViewManager", "Activity no longer valid for fragment transaction")
-                }
-            } catch (e: Exception) {
-                android.util.Log.e("SelfOCRViewManager", "Fragment transaction failed", e)
-            }
-        }
+        activity.window.decorView.post {
+            try {
+                if (!activity.isFinishing && !activity.isDestroyed) {
+                    val fm = activity.supportFragmentManager
+                    if (!fm.isStateSaved) {
+                        fm.beginTransaction()
+                            .setReorderingAllowed(true)
+                            .replace(reactNativeViewId, cameraFragment, reactNativeViewId.toString())
+                            .commit()
+                    } else {
+                        android.util.Log.w("SelfOCRViewManager", "FragmentManager state saved; aborting create for $reactNativeViewId")
+                    }
+                } else {
+                    android.util.Log.e("SelfOCRViewManager", "Activity no longer valid for fragment transaction")
+                }
+            } catch (e: Exception) {
+                android.util.Log.e("SelfOCRViewManager", "Fragment transaction failed", e)
+            }
+        }
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfOCRViewManager.kt
around lines 132 to 148, the code uses supportFragmentManager.commitNow which
can throw if the FragmentManager state is saved; instead check
activity.supportFragmentManager.isStateSaved before committing and if state is
saved skip or defer the transaction, otherwise perform a normal asynchronous
transaction using
beginTransaction().setReorderingAllowed(true).replace(...).commit() (not
commitNow) so the transaction is safe across saved state and respects
reordering.

}

private fun destroyFragment(root: FrameLayout, reactNativeViewId: Int) {
Expand Down Expand Up @@ -97,8 +174,8 @@ class SelfOCRViewManager(

private fun manuallyLayoutChildren(view: View) {
// propWidth and propHeight coming from react-native props
val width = requireNotNull(propWidth)
val height = requireNotNull(propHeight)
val width = propWidth ?: 800 // Default fallback
val height = propHeight ?: 800 // Default fallback

Comment on lines +177 to 179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Default 800x800 sizing may fight RN layout and cause over/under-sizing

Hard defaults in native (px) can conflict with RN dp sizing, especially since RCTFragment passes PixelRatio.getPixelSizeForLayoutSize(800) on the JS side. Consider deriving from view.measuredWidth/Height when available, falling back only if zero.

-        val width = propWidth ?: 800 // Default fallback
-        val height = propHeight ?: 800 // Default fallback
+        val measuredW = if (view.measuredWidth > 0) view.measuredWidth else null
+        val measuredH = if (view.measuredHeight > 0) view.measuredHeight else null
+        val width = propWidth ?: measuredW ?: 800
+        val height = propHeight ?: measuredH ?: 800
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val width = propWidth ?: 800 // Default fallback
val height = propHeight ?: 800 // Default fallback
val measuredW = if (view.measuredWidth > 0) view.measuredWidth else null
val measuredH = if (view.measuredHeight > 0) view.measuredHeight else null
val width = propWidth ?: measuredW ?: 800
val height = propHeight ?: measuredH ?: 800
🤖 Prompt for AI Agents
In
packages/mobile-sdk-alpha/android/src/main/java/com/selfxyz/selfSDK/SelfOCRViewManager.kt
around lines 163-165, the code currently hard-sets width/height to 800 which can
conflict with React Native layout; change the logic to use propWidth/propHeight
if provided, otherwise use the view's measuredWidth/measuredHeight when > 0, and
only fall back to the hard default (e.g., 800) if both prop and measured values
are zero; ensure the values are treated as pixels and, if necessary, convert
from DP to pixels consistently with RN before using them.

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
Expand Down
Loading