Feat: Upgrade to React Native 0.86 (New Architecture / TurboModule) + SPM opt-in for ZelloSDK#28
Conversation
2.0.0 Release
Increase Android SDK to 1.0.+ (1.0.4)
Increase RN to 2.0.1
Example app update
The example app pins iOS to UIUserInterfaceStyle=Light but Android used Theme.AppCompat.DayNight, so on Android in dark mode the hardcoded white modal dialogs rendered with the system white text, making content invisible. Match iOS by using the Light theme. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a pod install-time deprecation warning to the podspec and a README notice. CocoaPods still works and remains supported; Swift Package Manager is the recommended installation path going forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrade the library and example app from RN 0.74.5 to 0.86.0 (React 19.2.3). JS/tooling: - Bump RN, React, @react-native/* tooling, builder-bob 0.42, TypeScript 5.8 - Require Node >= 22.11 (.nvmrc -> v22) - Switch example SDK dep from file:.. to link:.. (avoids yarn pack OOM) - Map scoped package name to ./src in tsconfig paths - Migrate jest preset to @react-native/jest-preset - Fix eslint via plugin:prettier/recommended (0.86 config dropped the plugin) - Use requestNotifications() on Android (rn-permissions v5 dropped POST_NOTIFICATIONS) - Bump example deps (navigation 7, screens 4, safe-area-context 5, firebase 24, etc.) Android (config only, not build-verified - no local JDK/SDK): - SDK 36, buildTools 36.0.0, NDK 27.1.12297006, Kotlin 2.1.20, Gradle 9.3.1 - AGP 8.13.0, Hilt/Dagger 2.56.2, newArchEnabled=true Verified: typecheck, lint, bob build, and Metro JS bundle all pass. Native builds pending the TurboModule conversion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a single codegen spec (src/specs/NativeZelloSdk.ts) that both platforms will implement, replacing the two divergent legacy bridge modules. - configure takes one object param; getHistory/getHistoryImageData are Promises - events stay off the spec, emitted on the global DeviceEventEmitter 'zellosdk' channel - wire codegenConfig (RNZelloSdkSpec, outputDir ios/android); add @react-native-community/cli - mark create-react-native-library type as turbo-module; gitignore generated output Validated: 'yarn bob build --target codegen' generates the iOS NativeZelloSdkSpec protocol and Android NativeZelloSdkSpec.java. Native impls and JS rewiring follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the iOS TurboModule conforming to the generated NativeZelloSdkSpec:
- ZelloSdkModule.{h,mm}: ObjC++ module, RCT_EXPORT_MODULE(NativeZelloSdk),
getTurboModule -> NativeZelloSdkSpecJSI, forwards all 29 methods to Swift
- ZelloSdkModuleImpl.swift: Swift impl calling Zello.shared; configure takes
an object param, getHistory/getHistoryImageData use resolve/reject
- The existing ZelloIOSSdkModule RCTEventEmitter is left intact for events
(both share the Zello.shared singleton, no event-layer rewrite needed)
- Fix create-react-native-library type to module-new
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generated NativeZelloSdkSpec header is Obj-C++ only, but ZelloSdkModule.h is pulled into the pod's Swift umbrella. Move the spec import and protocol conformance into ZelloSdkModule.mm via a class extension, leaving the public header free of C++. Also exclude ios/generated (local bob codegen output) from the podspec so it is never compiled by the host app. Verified: the react-native-zello-sdk pod and both new files now compile clean in the example iOS build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace per-platform NativeModules method calls in src/sdk/index.ts with the unified NativeZelloSdk TurboModule: collapse ~25 Platform.OS branches to single calls, pass the whole config object to configure, and use the returned Promise for getHistory/getHistoryImageData. ZelloAndroidSdkModule/ZelloIOSSdkModule are kept only for the event emitter. Verified: typecheck, lint, and Metro bundle pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Import the pod's Swift header framework-style (use_frameworks!) via __has_include - Drop BUILD_LIBRARY_FOR_DISTRIBUTION=YES from the Podfile post_install: it forced Swift module-interface verification that broke RNScreens under use_frameworks! - Regenerate Podfile.lock / project for RN 0.86 pods (prebuilt core, Hermes, SPM integration); gitignore the machine-local .xcode.env.local Verified: 'xcodebuild ... -sdk iphonesimulator build' => BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under the bridgeless New Architecture, RCTEventEmitter's sendEvent/receiveEvent path is not callable. Emit the unified 'zellosdk' event through the global RCTDeviceEventEmitter via the injected RCTCallableJSModules (matching how the Android module already emits), and subscribe on the JS side with DeviceEventEmitter instead of new NativeEventEmitter(module). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Gemfile: allow CocoaPods 1.16 (drop the <1.15 cap); add nkf + tsort, which Ruby 3.4+/4.0 removed from stdlib but CocoaPods' CFPropertyList needs. - Podfile: build ZelloSDK's Swift deps (PhoneNumberKit, SnowplowTracker, CocoaLumberjack, PromisesSwift) with BUILD_LIBRARY_FOR_DISTRIBUTION so the prebuilt ZelloSDK can resolve their resilient symbols at launch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firebase was declared but never used in the example: no JS import, no AppDelegate configuration, no GoogleService-Info.plist, and the NotificationServiceExtension doesn't reference it. Removing it trims the dependency surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a ZELLO_USE_SPM=1 opt-in to the podspec: when set, ZelloSDK is pulled via Swift Package Manager (github.com/zelloptt/ios-mobile-sdk, product ZelloSDKUmbrella, up-to-next-major from 2.0.0) using RN's spm_dependency helper, instead of the CocoaPods 'pod ZelloSDK'. CocoaPods stays the default. Full CocoaPods removal isn't possible on RN 0.86 (RN itself still requires CocoaPods; SPM-only is an unshipped RN RFC, react-native-community/discussions-and-proposals#994), so this mirrors sentry-react-native's SENTRY_USE_SPM approach: SPM for the native SDK, CocoaPods for the RN layer. Verified: ZELLO_USE_SPM=1 pod install + xcodebuild resolves ZelloSDK 2.0.5 from SPM and the example builds, launches, and the SDK initializes - with no resilient-symbol dyld workaround needed (SPM binaries are prebuilt resiliently). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: New Architecture + Node prerequisites; Turbo Native Module intro; native ZelloSDK via CocoaPods (default) or SPM (ZELLO_USE_SPM=1, Sentry-style); the required BUILD_LIBRARY_FOR_DISTRIBUTION post_install for the CocoaPods path on Xcode 16/26+; and why full CocoaPods removal isn't possible on RN 0.86 yet. - CHANGELOG: 3.0.0 entry (New Arch breaking change, SPM opt-in). - Bump version 2.0.1 -> 3.0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rboModule - Extend the generated NativeZelloSdkSpec; drop @ReactMethod and the getName override. - Unify signatures to the shared codegen spec: configure(ReadableMap), selectContact (was setSelectedContact), getHistoryImageData (was getImageDataForHistoryImageMessage), split connectGroupConversation/disconnectGroupConversation; getHistory and getHistoryImageData resolve a Promise instead of invoking a Callback. - JS getSdk(): on Android the NativeZelloSdk TurboModule owns the Zello delegate; drop the now-stale ZelloAndroidSdkModule NativeModules reference. - Update the manual Hilt ReactPackage (README + example) to BaseReactPackage with a ReactModuleInfoProvider (isTurboModule = true). - eslintIgnore: skip build/, generated/, Pods/ dirs (SPM checkouts were being linted). Not built locally (no Android SDK/JDK) - to be verified in CI. connectGroupConversation assumes the Android SDK's connectChannel accepts a ZelloGroupConversation (flagged in code). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Trigger on master (was main — CI never ran on this repo's default branch). - build-ios: matrix over [cocoapods, spm] to verify both native-ZelloSDK install paths; install pods via bundle so the updated Gemfile (CocoaPods 1.16 + nkf/tsort) is honored, with RCT_NEW_ARCH_ENABLED=1; ZELLO_USE_SPM=1 for the spm variant. - Add jest unit tests for the binary-search/comparator helpers (the test job had zero tests and was failing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI surfaced these (Android can't be built locally — no SDK/JDK here):
- settings.gradle: the removed native_modules.gradle apply -> com.facebook.react.settings
plugin + autolinkLibrariesFromCommand().
- app/build.gradle: drop applyNativeModulesAppBuildGradle; use autolinkLibrariesWithApp()
inside the react {} block.
- MainApplication: SoLoader.init(this, OpenSourceMergedSoMapping) (the boolean overload is
gone in RN 0.76+).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Hilt Gradle plugin was bumped to 2.56.2 (Kotlin 2.1.20 compat) but the example app still pinned hilt-android / hilt-android-compiler 2.51. Dagger requires the plugin and runtime libs to match, so the build would fail. Bumped both to 2.56.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RN 0.86 requires Xcode >= 16.1; the macos-14 runner ships 15.4, which made use_react_native! abort with 'Please upgrade XCode'. macos-15 defaults to Xcode 16.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
compileDebugKotlin couldn't resolve NativeZelloSdkSpec because the library's
build.gradle was still the module-legacy structure. Add the module-new pieces:
- sourceSets generated/java + generated/jni so the generated spec compiles
- react { jsRootDir, libraryName = NativeZelloSdk, codegenJavaPackageName } so the
RN Gradle plugin generates the library codegen into that location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The library uses kapt (Hilt); the RN Gradle plugin adds build/generated/source/codegen/java via finalizeDsl, which the Kotlin/kapt compile doesn't reliably pick up, leaving NativeZelloSdkSpec unresolved. Register the codegen output dir on the main source set explicitly (replacing the wrong generated/java path). The other Kotlin libs (screens, safe-area-context) compile their codegen fine because they don't use kapt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…piles The library's only @Inject was on ZelloAndroidSdkModule, which the host app constructs manually (ZelloAndroidSdkPackage) — Hilt never processed it, so kapt was dead weight. With kapt enabled, the generated NativeZelloSdkSpec wasn't visible to kaptDebugKotlin/compileDebugKotlin, failing the build. Removing the Hilt plugin, kapt, the Hilt deps, and the vestigial @Inject lets the codegen spec compile like the other kapt-free Kotlin libs (screens, safe-area-context). The example app keeps Hilt (where Zello is actually injected and passed to the package). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…osdk The gradle plugin passes --javaPackageName com.zellosdk (from codegenConfig.android.javaPackageName), so RN generates the TurboModule spec as com.zellosdk.NativeZelloSdkSpec, not com.facebook.fbreact.specs.NativeZelloSdkSpec. Verified by running generate-specs-cli locally with that flag. Since the module is already in package com.zellosdk, the spec is same-package and needs no import; the old import was the real 'Unresolved reference NativeZelloSdkSpec' cause (kapt was a red herring). All 29 override signatures match the generated abstract methods. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The example app linked com.zello:sdk:1.0.4, whose POM declares hilt-android:2.52 (pre-2.55, so it never emits Dagger's LazyClassKey helpers). The app was on Hilt 2.56.2, which generates SingletonC code importing *_LazyMapKey classes that don't exist in the AAR -> :app:hiltJavaCompileDebug failed with 78 'cannot find symbol' errors. Pinned the app + gradle-plugin classpath to 2.52 to match the AAR exactly. (The library itself no longer uses Hilt, so it no longer constrains this version.) Verified the AAR's Hilt version by fetching its POM from the S3 maven. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hilt 2.52 matched the com.zello:sdk AAR but crashed :app:kaptDebugKotlin under Kotlin 2.1.20 (2.52 predates Kotlin 2.1). 2.54 is the newest release before Dagger's LazyClassKey became the default (2.55), so it both runs cleanly under Kotlin-2.1 kapt and avoids the _LazyMapKey references the pre-2.55 AAR lacks. Also set kapt.verbose=true so any remaining annotation-processor error surfaces in the CI log instead of a generic worker failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Zello SDK The com.zello AARs are built with Dagger 2.52, whose @LazyClassKey impl predates the per-binding *_LazyMapKey proxy classes. The example app must use Dagger >= 2.54 (RN 0.86 forces Kotlin 2.1.20; Dagger 2.52 crashes kapt under it), and >= 2.53 consumes the SDK's @LazyClassKey ViewModel bindings by referencing those proxies from DaggerMainApplication_HiltComponents_SingletonC -> :app:hiltJavaCompileDebug failed with 78 'cannot find symbol ... _LazyMapKey' errors. Generate the 78 missing keeper classes (39 ViewModels x BindsModule_Binds + KeyModule_Provide), matching Dagger's LazyMapKeyProxyGenerator output, by scanning the SDK AARs for *_HiltModules classes. Workaround for a stale prebuilt SDK; remove once the Zello Android SDK ships a Dagger >= 2.53 build (which emits these itself). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| push: | ||
| branches: | ||
| - main | ||
| - master |
There was a problem hiding this comment.
@zello-amiran looks like we did not migrated master to main although main referenced ?
cc: @bcorralejo-zptt
There was a problem hiding this comment.
@rxmichael can we run this in our CI/CD environment to make sure it is going to work?
There was a problem hiding this comment.
This has CI jobs for all three variants (see checks)
|
|
||
| apply plugin: "com.android.library" | ||
| apply plugin: "kotlin-android" | ||
| apply plugin: 'com.google.dagger.hilt.android' |
There was a problem hiding this comment.
are we sure we are okay removing hilt?
(it was a requirnment in the past)
There was a problem hiding this comment.
generally wise I think it's correct to remove hilt from library dependencies list IMO
There was a problem hiding this comment.
is the idea to to have these classes instead of hilt?
Let's maybe leave hilt in the app rather then add generated shims?
@zello-eugene @padgrayson91 what do you think?
| #### Linking | ||
|
|
||
| Since the Zello Android SDK requires Hilt, and we must inject the `Zello` instance into the `ReactPackage`, the project does not work with autolinking. | ||
| Since the Zello Android SDK requires Hilt, and we must inject the `Zello` instance into the package, the project does not work with autolinking. |
There was a problem hiding this comment.
looks like we did remove hilt from deps?
There was a problem hiding this comment.
The consumer app's Hilt (what the README shows — @HiltAndroidApp, @Inject lateinit var zello, ZelloAndroidSdkPackage @Inject constructor(zello)): still required, still accurate. The app must use Hilt because that's the only way the Zello Android SDK hands you a Zello, and the package can't be autolinked since it needs that instance.
| // com.zello:sdk AAR (built with Hilt 2.52), and 2.52 itself crashes kapt | ||
| // under Kotlin 2.1.20. 2.54 satisfies both: Kotlin-2.1-compatible kapt and | ||
| // no LazyClassKey output. | ||
| implementation("com.google.dagger:hilt-android:2.54") |
There was a problem hiding this comment.
Let's maybe leave hilt in the app rather then add generated shims?
@zello-eugene @padgrayson91 what do you think?
| } | ||
|
|
||
| override fun getName() = "ZelloAndroidSdkModule" | ||
| // getName() is provided by NativeZelloSdkSpec (returns "NativeZelloSdk"). |
There was a problem hiding this comment.
Is it safe to change the name?
There was a problem hiding this comment.
A New Arch TurboModule is defined by one codegen spec shared by both platforms, so it can't keep two different names — it needs a single platform-neutral one. That's why everything now resolves to NativeZelloSdk (Native* is the RN spec-file naming convention; the JS does getEnforcing('NativeZelloSdk') once, no branching).
| // getName() is provided by NativeZelloSdkSpec (returns "NativeZelloSdk"). | ||
|
|
||
| @ReactMethod fun configure(enableOfflineMessagePushNotifications: Boolean, enableForegroundService: Boolean) { | ||
| override fun configure(config: ReadableMap) { |
There was a problem hiding this comment.
Is this mandatory? It's significantly worse for readability and type clarify on what the options are
| @ReactMethod fun startVoiceMessage(name: String, type: String) { | ||
| val contact = contactFrom(name, type) ?: return | ||
| override fun connectGroupConversation(name: String) { | ||
| // NOTE: confirm against the Android SDK that connectChannel accepts a |
There was a problem hiding this comment.
We need to confirm this before merge. This also looks like new functionality which is odd for this PR
|
|
||
| private let zello = Zello.shared | ||
|
|
||
| @objc public func configure(_ config: NSDictionary) { |
There was a problem hiding this comment.
An untyped arbitrary dictionary isn't great
|
@rxmichael we need to target |
…ectChannel Verified against com.zello:sdk:1.0.4 bytecode: ZelloGroupConversation extends ZelloChannel and Zello.connectChannel/disconnectChannel take a ZelloChannel, so passing a group conversation is a valid upcast. Resolves the prior TODO note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TurboModule boundary stays NSDictionary (the codegen spec uses UnsafeObject because configure() is platform-specific and iOS only reads the 'ios' slice), but the untyped key lookups are now centralized in a typed private Config struct so the method body is type-safe and the payload-shape assumptions live in one place. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed against the ZelloSDK.xcframework swiftinterface: Zello.configuration is a settable property and there is no configure() method — configuration is applied by assignment. Master (and the migrated code) built a ZelloConfiguration with the appGroup and pushNotificationEnvironment but never assigned it, so those settings were silently ignored on iOS (push env always defaulted to .production, app group never set). Assign zello.configuration so configure() takes effect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The root npm lockfile was stale from before the migration (pinned react-native 0.74.5 / react 18.2.0). Regenerated with 'npm install --package-lock-only' so it matches the current package.json and yarn.lock resolution: react-native 0.86.0, react 19.2.3, @react-native/jest-preset 0.86.0, no 0.74.x references. The example workspace's SDK dep is recorded as file:.. (npm's local-symlink form; the package.json keeps yarn's link:.. and is unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Podfile.lock was committed stale at 2.0.1 while the podspec is 3.0.0, so :app:CheckPodsManifest failed the iOS build with 'sandbox not in sync'. Updated the lock to match the installed Pods sandbox (Manifest.lock, 3.0.0). Verified: the example app builds and launches on the iOS simulator afterward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The create/add-users dialogs used react-native-paper's adaptive Checkbox, which renders Checkbox.IOS on iOS -- that variant shows only a checkmark when checked and nothing (no box outline) when unchecked, so the rows looked empty on the simulator. Use Checkbox.Android explicitly, which draws a visible box in both states on every platform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the create/add-users group-conversation dialogs the user row is a space-between flex row; a long displayName grew unbounded and pushed the checkbox off the right edge. Cap userName at width: '70%' so the checkbox always has room. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Upgrades the SDK from React Native 0.74.5 to 0.86 on the New Architecture, converts the native modules to a Turbo Native Module, and adds an opt-in Swift Package Manager path for ZelloSDK. Released as 3.0.0 (breaking).
IOS-4249
What changed
src/specs/NativeZelloSdk.ts); iOS reimplemented as an ObjC++ module forwarding to Swift; AndroidZelloAndroidSdkModulereimplemented againstNativeZelloSdkSpec. JS de-branched (~25Platform.OSsites) onto the unified module;getHistory/getHistoryImageDataare now Promises; events flow over the bridgeless-safeDeviceEventEmitter.ZELLO_USE_SPM=1pulls ZelloSDK fromzelloptt/ios-mobile-sdk(ZelloSDKUmbrella) viaspm_dependency. CocoaPods stays the default. Full CocoaPods removal isn't possible on RN 0.86 (RN still requires it — RFC #994).BUILD_LIBRARY_FOR_DISTRIBUTIONrequirement for Xcode 16/26+; CHANGELOG 3.0.0.Verified
bob build.Needs CI (couldn't build locally — no Android toolchain)
connectGroupConversation— assumeszello.connectChannel(...)accepts aZelloGroupConversation(flagged in code).Breaking
Follow-ups (non-blocking)
ZelloSDKUmbrellain SPM mode.RCTEventEmitter.receiveEventred box (third-party view-interop, not the SDK).EventEmitterdeep-import lint warning insrc/sdk/index.ts:43.