👍🎉 First off, thanks for taking the time to contribute! 🎉👍
We love the open source community and enjoy the support and contributions of many of our users. We ask that any potential contributors to the SDK Follow the following guidelines:
If your proposed contribution is a small bug fix, please feel free to create your own fork of the repository and create a pull request.
If your contribution would break or change the functionality of the SDK, please reach out to us on (contact) before you put in a lot of effort into a change we may not be able to use. We try our best to make sure that the SDK remains stable so that developers do not have to continually change their code, however some breaking changes are desirable, so please get in touch to discuss your idea before you put in a lot of effort.
The shared Kotlin Multiplatform :kmp module lives in the standalone OneSignal-KMP-SDK repository and is consumed here as a git submodule pinned to a specific commit. Its sources are checked out under OneSignal-KMP-SDK/, so you must initialize the submodule before building or the Gradle build will fail (the remapped :OneSignal:kmp project directory won't exist).
# Fresh clone (recommended)
git clone --recurse-submodules git@github.com:OneSignal/OneSignal-Android-SDK.git
# If you already cloned without --recurse-submodules
git submodule update --init --recursiveWhenever you pull changes that move the submodule pointer, re-run git submodule update --init --recursive to sync your local checkout to the pinned commit.
The :kmp sources live in the KMP repo and are checked out here, nested, at OneSignal-KMP-SDK/. This means two gits are in play:
- Commands run from the Android repo root act on Android and only record the submodule pointer (a
160000gitlink SHA) — not the logger source. - Commands run from inside
OneSignal-KMP-SDK/act on the KMP repo — that's where the actual.ktchanges are committed.
Because Gradle reads the module from the submodule folder on disk, your uncommitted KMP edits compile straight into the Android app — no push or version bump is needed while iterating.
There are two distinct flows: everyday development (Flow A) and releasing (Flow B).
Ends when your change is on KMP main. No tagging and nothing ships to Android here.
flowchart TD
A["Clone with --recurse-submodules<br/>(or: git submodule update --init --recursive)"] --> B["cd OneSignal-KMP-SDK<br/>git checkout main && git pull<br/>git checkout -b feat/my-logger-change"]
B --> C["Edit :kmp sources"]
C --> D["Build/verify locally (no push)<br/>Android: ./gradlew :OneSignal:core:compileDebugKotlin<br/>KMP tests: ./gradlew :kmp:testDebugUnitTest :kmp:iosSimulatorArm64Test spotlessCheck"]
D -->|iterate| C
D --> E["Inside the submodule: commit + push<br/>cd OneSignal-KMP-SDK<br/>git add -A && git commit -m 'feat(logger): ...'<br/>git push -u origin feat/my-logger-change"]
E --> F["Open KMP PR — CI runs (Spotless + unit tests)"]
F --> G["Merge to KMP main"]
G --> H(["Change is on KMP main — still NO tag"])
The submodule starts in detached HEAD at the pinned commit. Run
git checkout main(or-b <feature>) insideOneSignal-KMP-SDK/before committing, or your commit will be orphaned.
Run this when a merged KMP main should actually ship into the Android (and later iOS) SDK. Manual trigger; Android itself is never tagged — only its submodule pointer moves to the KMP tag.
flowchart TD
A["KMP main has the change(s) to release"] --> B["KMP repo → Actions → 'Release' → Run workflow<br/>pick bump: patch / minor / major"]
B --> C["Release workflow (automatic):<br/>verify → compute next vX.Y.Z<br/>→ create tag + GitHub Release (auto notes)<br/>→ call Android bump workflow (GH_PUSH_TOKEN)"]
C --> D["Android PR opens: 'bump submodule → vX.Y.Z'<br/>moves the pointer to the tag's commit<br/>body includes the KMP release notes · NOT auto-merged"]
D --> E["YOU: review the notes + merge the Android PR"]
E --> F["Sync local Android checkout:<br/>git pull && git submodule update --init --recursive"]
F --> G(["Android pinned to KMP tag vX.Y.Z"])
The only manual actions in Flow B are Run workflow and merge the bump PR; everything in between is automated.
The published modules track their public API in committed dumps under <module>/api/*.api, generated by the binary compatibility validator. CI runs apiCheck alongside Spotless and Detekt, so any change to the public surface fails the build until the dump is regenerated. The point is that a breaking change has to appear as a reviewable diff rather than be caught by eye.
If you change the public API on purpose, regenerate and commit the dumps:
cd OneSignalSDK
./gradlew apiDumpThen read the resulting diff before committing it. A removed or altered line is a breaking change for customers. An added line is usually safe, with one important exception: a new method on an interface customers implement (listeners such as INotificationClickListener or IUserJwtInvalidatedListener) breaks every existing implementation.
Two things about the dumps are worth knowing up front:
- They cover customer and wrapper-SDK API only. Kotlin has no cross-module "internal", so
com.onesignal.**.internal.*and most ofcom.onesignal.common.*compile as public;apiValidationinOneSignalSDK/build.gradleexcludes them so internal refactors don't tripapiCheck.OneSignalWrapper,IHostPermissionPrompt, anddebug.internal.logging.Loggingstay in because the wrapper SDKs (Flutter, React Native, Cordova, Capacitor, Unity, .NET) call them, so treat them as public API. If you add a new internal package, or a new class at the root ofcom.onesignal.common, add it toignoredPackagesorignoredClassesrather than dumping it. in-app-messages.apiandlocation.apiare empty because their customer-facing interfaces live incore. That is expected, not a misconfiguration.notifications.apialso records manifest components (receivers, activities, HMS/ADM services).Logging.setLoggerTelemetrytakes a KMP type, so a KMP submodule bump (Flow B above) that changesILogTelemetryRemotewill failapiCheck. RunapiDumpon the bump PR if so.
The validator hooks into the kotlin-android plugin, and CI calls each module's apiCheck by path. If a module moves to AGP 9's built-in Kotlin, its task stops being registered and CI fails with "task not found" rather than skipping it. See BCV#312.
The eventual replacement is the ABI validation built into the Kotlin Gradle plugin (kotlin { abiValidation { } }, experimental since Kotlin 2.2). It keeps the same api/*.api dump format, so the move is mostly porting the apiValidation filters. It needs Kotlin 2.2+, so it is planned alongside the AGP 9 move for v6.
Before creating bug reports, please check this list of steps to follow.
- Make sure that you are actually encountering an issue and not a question. If you simply have a question about the SDK, we would be more than happy to assist you in our Support section on the web (https://www.onesignal.com - click the Message button at the bottom right)
- Please make sure to include as many details as possible
Note: If you find a Closed issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
- Use a clear and descriptive title for the issue to identify the problem.
- Include Reproducibility It is nearly always a good idea to include steps to reproduce the issue. If you cannot reliably reproduce the issue yourself, that's ok, but reproducible steps help best.
- Describe your environment, tell us what version of the Android SDK you are using, how you added it to your project (ie. maven or manually adding it to your project), what version of Android you are targeting, and so on.
- Include a Stack Trace If your issue involves a crash/exception, PLEASE post the stack trace to help us identify the root issue.
- Include an Example Project This isn't required, but if you want your issue fixed quickly, it's often a good idea to include an example project as a zip and include it with the issue. You can also download the Demo project (included in the
/Examplesfolder of this repo) and set up an example project with this code as a starting point.