Skip to content

fix(react-native-mdocs-holder-tutorial): repair Android debug build - #341

Merged
avner-m merged 2 commits into
masterfrom
fix/rn-holder-tutorial-android-debug-build
Sep 14, 2026
Merged

avner-m merged 2 commits into
masterfrom
fix/rn-holder-tutorial-android-debug-build

Conversation

@avner-m

@avner-m avner-m commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

The completed React Native mDocs Holder tutorial app could not run on Android. A debug build installed successfully and then crashed on startup before the first screen rendered. This fixes that, plus two related defects found while tracing it.

Remove expo-dev-client

This is the cause of the startup crash. The app died during React Native TurboModule initialization with:

java.lang.NoClassDefFoundError: Failed resolution of: Lkotlinx/datetime/Clock$System;
  at global.mattr.mobilecredential.holder.MobileCredentialHolder.<init>
  at com.mobilecredentialholderreactnative.MobileCredentialHolderReactNativeModule.<init>

The native Holder AAR (global.mattr.mobilecredential:holder:7.1.0, pinned by RN Holder SDK 10.0.0) requests kotlinx-datetime:0.6.0 and its bytecode contains a hard reference to kotlinx.datetime.Clock$System. expo-dev-launcher, pulled in transitively by expo-dev-client, declares kotlinx-datetime:0.7.1 as a debugOnly dependency. Gradle resolves version conflicts to the highest version, so 0.7.1 wins.

That upgrade is binary incompatible. In 0.7.x, kotlinx.datetime.Clock was demoted to a deprecated typealias for kotlin.time.Clock, and a typealias has no runtime class. The 0.6.0 jar ships kotlinx/datetime/Clock.class and Clock$System.class; the 0.7.1 jar ships neither. The AAR's reference therefore resolves to nothing.

The two versions cannot coexist. Only one can be on an Android runtime classpath, and forcing 0.6.0 instead breaks :expo-dev-launcher:compileDebugKotlin, because its DateFormat.kt calls the format builder method day(), which only exists in 0.7.x (0.6.0 has dayOfMonth()). There is also no newer Holder SDK to move to: 10.0.0 is the latest published and it pins native Android 7.1.0.

Since expo-dev-client is the only package pulling in expo-dev-launcher, and nothing in the app source imports it, removing it is the one option that leaves both libraries at a version they were compiled against. expo run:android still produces a development build with Metro and fast refresh. Only the dev launcher UI for switching dev servers is lost, which a tutorial reader running a single project does not need.

Because the conflict is scoped debugOnly, release builds were always unaffected. This only ever broke the debug builds that anyone following the tutorial would produce.

Derive the app scheme from config in withAndroidHolderSDK.js

The plugin hardcoded io.mattrlabs.sample.reactnativemobilecredentialholdertutorialapp in two places rather than reading config.android.package. The tutorial instructs readers to change the bundle identifier, and doing so silently broke two things:

  • WebCallbackActivity stayed registered on the original scheme, so OAuth redirects arrived on a scheme nothing was listening for and the credential offer flow failed with no error pointing at the cause.
  • The MainActivity intent filter cleanup stopped matching anything and became a no-op, so its stated purpose of preventing MainActivity from swallowing OAuth callback URLs was not being served.

Both now read the package from config, so a rename stays consistent across the generated manifest.

Register the development launch deep link

With expo-dev-client removed, expo run:android built and installed the APK and then failed at the final step:

Error: Activity not started, unable to resolve Intent
  { act=android.intent.action.VIEW
    dat=io.mattrlabs...tutorialapp://expo-development-client/... }

Expo CLI builds its launch URL from the longest scheme in the manifest (resolveExpoOrLongestScheme), which is the Android package. That scheme was only registered with android:host="accept", so the CLI's expo-development-client intent matched nothing. expo-dev-client's own config plugin used to register a host-less filter that absorbed this, which is why the gap only became visible once it was removed.

The plugin now registers that single host on MainActivity. Scoping it to expo-development-client specifically, rather than restoring a host-less catch-all, keeps expo run:android working without letting MainActivity intercept the OAuth callback URLs that WebCallbackActivity needs.

Sync yarn.lock

package.json already pinned @mattrglobal/mobile-credential-holder-react-native@^10.0.0, but the committed lockfile still resolved 8.1.2. Anyone installing from the lockfile got a Holder SDK two majors behind the manifest. The lockfile now matches.

The same fixes in the starter app

The starter ships a byte-identical withAndroidHolderSDK.js and the same expo-dev-client dependency and stale lockfile, so the three changes above apply to it unchanged. Its app.config.ts is deliberately left alone, because the differences there are tutorial scaffolding rather than defects.

The starter does behave differently in its shipped state, which affects when each problem surfaces rather than whether it exists:

  • withAndroidHolderSDK.js is present but not yet listed in plugins; the reader registers it partway through the tutorial. Until then none of its mods run, and Android cannot resolve the Holder AAR at all, since the Maven repository injection is what makes it resolvable.
  • scheme is still the default and there are no intentFilters, so the longest scheme in the generated manifest is host-less and the CLI's launch deep link resolves. The unresolved intent error only appears once the reader updates the scheme and adds the intent filters, at which point the app matches the completed sample exactly.

In other words, a reader who follows the tutorial to the end lands in precisely the state that was broken, so leaving the starter unfixed would just defer the same three failures.

No other sample app in this repository combines expo-dev-client with a MATTR mDocs SDK, so the scope ends here.

Testing

Verified on a physical Android device (Samsung SM-A057F, Android 15):

  • kotlinx-datetime resolves to 0.6.0 on debugRuntimeClasspath, with no conflict to resolve and no force override
  • Debug build compiles, installs, and launches
  • logcat shows no NoClassDefFoundError and no reference to kotlinx.datetime or mobilecredential
  • The generated manifest carries the correct scheme for accept, credentials, and expo-development-client hosts, under both the default package name and a renamed one
  • expo run:android completes without the unresolved intent error

🤖 Generated with Claude Code

Remove expo-dev-client, whose kotlinx-datetime 0.7.x requirement is
binary incompatible with the 0.6.0 the Holder AAR is built against,
sync the stale lockfile, and derive the app scheme from config in
withAndroidHolderSDK so renaming the package no longer breaks OAuth.
@avner-m
avner-m requested a review from a team as a code owner September 14, 2026 03:32
…starter

The starter ships the same withAndroidHolderSDK.js and the same
expo-dev-client dependency, so a reader following the tutorial hits the
identical kotlinx-datetime conflict and package rename defects once they
register the plugin. Its app.config.ts is left untouched, as the
differences there are deliberate tutorial scaffolding.
@avner-m
avner-m merged commit 4af6191 into master Sep 14, 2026
2 checks passed
@avner-m
avner-m deleted the fix/rn-holder-tutorial-android-debug-build branch September 14, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants