# OneSignal SDK Integration Guide You are an expert SDK integration agent with direct access to the developer's codebase and git repository. ## Context The platform for this app (Android / iOS / Flutter / Unity / React Native / Web) is already known from the editor context or the URL parameter. **Do NOT ask which platform this is.** Your task is to **fully integrate the OneSignal SDK** into this repository using official documentation and best practices from: * [OneSignal Website](https://onesignal.com/) * [Mobile SDK reference](https://documentation.onesignal.com/docs/en/mobile-sdk-reference) * [Web SDK setup](https://documentation.onesignal.com/docs/web-sdk-setup) * [OneSignal Documentation](https://documentation.onesignal.com/) * [OneSignal SDKs](https://github.com/OneSignal/sdks) --- ## App ID The App ID is provided in the user's prompt — the same message that linked you to this file. Use **that** App ID for all OneSignal SDK initialization and any REST calls in the verification scaffolding. If no App ID is present in the user's prompt, ask the user to provide one before proceeding. **Never** hardcode a demo or fallback App ID. --- ## Step 1 — Detect First, Then Ask ONLY What Is Unknown Before editing files, inspect the repository and record a short readiness picture. Prefer detection over questions. If an answer is already clear from the user's request, the repo, or the execution context, do NOT ask — proceed and state the assumption in your summary. ### Detect-first checklist (run before changes) Inspect the project and note: * Existing OneSignal usage (dependencies, init calls, wrappers) * iOS bundle identifier(s) and Android applicationId / package name as defined in the project today * iOS dependency manager in use: CocoaPods (`Podfile` / `Pods` / Pods xcconfig includes) vs Swift Package Manager (no Podfile or SPM-enabled Flutter/iOS setup, package references) * JS package manager when relevant (npm / yarn / pnpm / bun) from the lockfile * Existing push setup: Notification Service Extension, App Groups, Push / Background Modes capabilities, notification permission prompts * Signing clues on iOS targets (`DEVELOPMENT_TEAM`, `CODE_SIGN_STYLE`, entitlements files) ### Questions to ask only when unknown 1. **What language is the app written in?** (if applicable) - Android: Kotlin or Java - iOS: Swift or Objective-C - Flutter: Dart (no choice needed) - Unity: C# (no choice needed) - React Native / Expo: JavaScript or TypeScript - Web: JavaScript or TypeScript (and detect the framework — plain HTML/JS, React, Next.js, Vue, Angular, Svelte, etc.) 2. **Which mobile platforms should this integration cover?** (cross-platform SDKs only: Flutter, React Native, Expo, Unity) - Ask: **iOS**, **Android**, or **both** - Do **not** offer a code-only / skip-native option - Native iOS and native Android prompts skip this question — the platform is already known - Apply shared SDK / wrapper work once; apply native iOS or Android project work only for the selected platform(s) - In the final summary, report status per selected platform (shared code, iOS native, Android native) 3. **How would you like to handle version control?** (only ask if the project has a git repository) - First, detect if the folder has a `.git` directory - If git is detected, ask: "Would you like me to stash any current changes and create a new branch called `onesignal-integration` for this work? Or should I write the changes directly to the current branch?" - **Option A: New branch** — Stash existing changes, create and switch to `onesignal-integration` branch, commit all changes there, do NOT push to main/master directly - **Option B: Current branch** — Write all changes directly to the current branch without stashing or creating a new branch - If no git repository is detected, skip this question and proceed ### Bundle ID and application ID (Required) * **Use the iOS bundle identifier and Android applicationId / package name already defined in the project.** Do not invent, rename, or "improve" them (including swapping to example or OneSignal-owned IDs) unless the user explicitly asks. * Derive the default App Group from the existing main-app bundle ID: `group.{MAIN_APP_BUNDLE_ID}.onesignal`. If the project already defines a custom App Group for OneSignal, keep it and set `OneSignal_app_groups_key` as documented in the iOS push infrastructure section. * If a required identifier is missing or clearly unusable for the selected platform(s), stop and ask the user to set a real value in the project, then continue. Do not proceed by making one up. ### Package manager continuity (Required) * Detect the project's existing dependency managers and **keep using them**. * iOS: preserve CocoaPods vs Swift Package Manager. Do not migrate the app between them unless the user asks. * JavaScript apps: use the lockfile's package manager (npm / yarn / pnpm / bun). * Do **not** add an NSE-only CocoaPods `Podfile` to an SPM-based iOS/Flutter project. Follow the platform section for how to link the Notification Service Extension under the detected manager. --- ## Push Subscription Verification Dialog (Required) After completing SDK initialization, add a push subscription observer so the app can confirm that the device registered successfully. ### Requirements (All Platforms) 1. **Register a push subscription observer** immediately after OneSignal is initialized. 2. **Retain the observer for the lifetime of the screen/component that owns it.** OneSignal stores push subscription observers weakly — a local-only variable is deallocated immediately and the dialog never appears. Store it in framework-retained state (for example SwiftUI `@State`, a UIKit view-controller property, React `useRef` / component field, Flutter `State` field, or an Android/`Application`-scoped reference). 3. **Treat the device as registered only when the push subscription ID is a real, server-assigned value** — non-empty and **not** prefixed with `local-`. The SDK assigns a `local-` placeholder ID during initialization (before the device registers with OneSignal's servers); that placeholder does **not** mean the device is registered. 4. **Evaluate the current subscription ID both on change and immediately at observer-registration time.** The ID may already be server-assigned before your observer attaches, so reacting only to the change event can miss the transition and the dialog would never appear. 5. **When a real subscription ID is present, show a dialog exactly once** (guard with a "shown once" flag) — a platform-native alert on mobile, or an in-page modal on web — with: - **Title:** "Your OneSignal SDK integration is complete!" - **Message:** "You can now send Push Notifications & In-App Messages through OneSignal. Tap below to enable push notifications." - **Single button:** **"Got it"** 6. **On button tap**, request push permission. ### Verification without duplicate subscriptions When building, launching, or re-checking the integration: * **Do NOT** clear app data, wipe the emulator/simulator, or uninstall/reinstall the app solely to re-verify setup. * Clearing storage / reinstalling resets the install identity. The platform then mints a new push token and OneSignal creates a **new** subscription, leaving the previous one orphaned — so the dashboard shows duplicates for the same test flow. * Prefer a normal rebuild + relaunch (or install over the existing app) that **preserves** app storage. * One successful server-assigned subscription and one verification dialog is enough. Do not deliberately create a second subscription to prove the integration works. > **Platform note:** Registration timing differs across platforms. On the mobile SDKs the device can register (and receive a real subscription ID) *before* push permission is granted, so the dialog gates the permission request. On **web**, a push subscription ID is typically only assigned *after* the user opts in, so the web integration file adapts this flow — see it for the exact, platform-correct implementation. See platform-specific integration files for implementation examples. --- ## Step 2 — SDK Version Selection **IMPORTANT:** Get SDK versions ONLY from this official JSON endpoint: **https://onesignal.github.io/sdk-releases/releases.json** * Do NOT use the human-readable releases page for version selection * Do NOT search the web for SDK versions * Do NOT guess versions * Do NOT use other sources (npm, pub.dev, GitHub releases) for version numbers * The official JSON endpoint above has both **Stable** and **Current** versions for all platforms * iOS note: `OneSignal-XCFramework` (the SPM repo) shares version tags with `OneSignal-iOS-SDK` — the same version number applies to both Use the **Stable** track unless the user specifically requested Current. Always pin that exact version from the releases JSON (SPM `exactVersion`, exact npm/pub version, exact CocoaPods version like `'5.2.0'`). Do not invent or widen version ranges. When using the JSON source, read the exact version from: `.channels..version` Find the SDK entry by matching the requested platform against the entry's `name` or `displayName`. For example, for iOS Stable, find the entry with `name: "iOS"` and use `channels.stable.version`. Do not infer the Stable version from the newest tag, GitHub release order, GitHub `prerelease` status, or semantic-version sorting. --- ## Step 3 — Architecture Compliance (Required) * Follow the **existing architecture of the codebase** * Do NOT introduce a new architectural pattern * If architecture is unclear, infer it from existing code and remain consistent Platform-specific guidance is provided in the platform integration files. --- ## Step 4 — Centralized OneSignal Integration (Required) Create a **single, centralized class/module** that wraps all OneSignal SDK interactions: ### Responsibilities * Initialize the SDK * Manage user identity (login/logout) * Handle email and SMS subscriptions * Manage user tags * Control logging levels * Isolate all direct OneSignal SDK calls ### Rules * **No direct OneSignal SDK calls outside this wrapper** * All OneSignal interactions go through the centralized class/module * Some platform-file code examples (e.g. the Push Subscription Verification Dialog) show direct SDK calls for brevity — when implementing them, route those calls through the wrapper * Makes testing and future SDK updates easier See platform integration files for specific implementation patterns and method signatures. --- ## Step 5 — Integration Implementation Perform a **minimal, production-ready integration**, including: 1. **Dependency configuration** (gradle, CocoaPods, pubspec, etc.) 2. **Required app config changes** (manifest, plist, etc.) 3. **Required notification infrastructure** — everything the platform integration file marks as **Required** (e.g. on iOS: the Notification Service Extension target and App Group). "Minimal" means no optional extras — it does **NOT** mean skipping required platform setup 4. **SDK initialization** at the correct lifecycle point 5. **Avoid deprecated APIs** — use the latest SDK patterns --- ## Step 6 — Changes Summary (Output in Chat) Output a **clean, copy-ready Pull Request summary** in the chat. Do NOT automatically create a PR — let the user copy it. ### Include in the summary: * Key changes made to the codebase * SDK details (platform, version, track) * Architecture decisions and where OneSignal logic is placed * For cross-platform SDKs: which mobile platforms were selected and status for each (shared code, iOS native, Android native) * Bundle ID / applicationId used from the project (do not invent new ones in the summary either) * Step-by-step verification instructions (simulator is fine for build/launch and the verification dialog path; on Apple Silicon, sandbox APNs in Simulator is also valid — see Shared iOS Push Infrastructure) * Any follow-ups, limitations, or known risks relevant to the code changes made --- ## Constraints * **Do NOT refactor unrelated code** * **Do NOT add optional OneSignal features** unless required. Anything the platform integration file marks as **Required** (e.g. the iOS Notification Service Extension and App Group) is part of the core integration — NOT an optional feature — and MUST be implemented * **Do NOT add push-notification features beyond SDK initialization, the Push Subscription Verification Dialog, and the sections the platform integration file marks as Required.** The dialog's on-tap permission request (Step 6 of the verification requirements) is required and is the **only** place push permission may be requested — do NOT prompt for permission at app launch or anywhere else * **Do NOT clear app data, wipe the emulator/simulator, or uninstall/reinstall solely to re-verify** — that creates duplicate OneSignal subscriptions (see Verification without duplicate subscriptions) * **Keep changes scoped, clean, and reviewable** * **Favor consistency** with the existing codebase * **Do NOT commit secrets** (API keys should be in environment variables or secure storage) --- # Android Platform Integration ## Official Documentation * [OneSignal Android SDK Setup](https://documentation.onesignal.com/docs/android-sdk-setup) * [Android SDK API Reference](https://documentation.onesignal.com/docs/android-sdk-api-reference) * [GitHub Repository](https://github.com/OneSignal/OneSignal-Android-SDK) --- ## Pre-Flight Checklist Before considering the integration complete, verify ALL of the following: ### Permissions - [ ] `INTERNET` permission in `AndroidManifest.xml` ```xml ``` - [ ] `POST_NOTIFICATIONS` permission for Android 13+ (SDK handles this, but verify it's not blocked) - [ ] **Open the app `AndroidManifest.xml` and inspect the `` tag.** If it has `tools:node="replace"`, **stop and warn the user before continuing.** That merger marker wipes library-merged components. OneSignal's `PermissionsActivity` never reaches the merged manifest, and the notification permission flow crashes with `ActivityNotFoundException`. **Fix:** remove `tools:node="replace"`. If they only need to override one attribute (theme, label, icon), use `tools:replace="android:theme"` (or that attribute) instead. Do not add `tools:node="replace"` yourself. After the next build, confirm `com.onesignal.core.activities.PermissionsActivity` is in `app/build/intermediates/merged_manifests`. ### Build Configuration - [ ] `minSdkVersion` is 21 or higher - [ ] `compileSdkVersion` is 33 or higher (recommended) Note: Do NOT add the Google Services Gradle plugin or a `google-services.json` file for OneSignal — the SDK registers for FCM itself and these files are not required. ### ProGuard/R8 (if minification is enabled) - [ ] OneSignal ProGuard rules are included (SDK usually handles this automatically) - [ ] Test release builds to ensure nothing is stripped incorrectly ### Initialization - [ ] OneSignal is initialized in `Application.onCreate()` or via AndroidX Startup - [ ] Initialization happens BEFORE any other OneSignal calls --- ## Architecture Guidance ### MVVM (Recommended) If the project uses MVVM architecture, place OneSignal logic in: ``` app/ ├── src/main/java/com/example/ │ ├── data/ │ │ └── repository/ │ │ └── NotificationRepository.kt # OneSignal wrapper │ ├── di/ │ │ └── AppModule.kt # Provide as singleton │ └── MyApplication.kt # Initialize here ``` ### Clean Architecture ``` app/ ├── data/ │ └── notification/ │ └── OneSignalNotificationService.kt ├── domain/ │ └── notification/ │ └── NotificationService.kt # Interface └── di/ └── NotificationModule.kt ``` ### Simple/No Architecture ``` app/ ├── src/main/java/com/example/ │ ├── OneSignalManager.kt # Singleton wrapper │ └── MyApplication.kt ``` --- ## Threading Model ### Kotlin (Coroutines) ```kotlin class OneSignalManager @Inject constructor( private val appContext: Context, @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) { suspend fun initialize(appId: String) = withContext(ioDispatcher) { // Set log level for debugging (remove in production) OneSignal.Debug.logLevel = LogLevel.VERBOSE // Initialize OneSignal OneSignal.initWithContext(appContext, appId) } suspend fun login( externalId: String, profile: OneSignalUserProfile ): OneSignalResult = withContext(ioDispatcher) { OneSignal.login(externalId, profile) } } ``` ### Java (Executors) `OneSignal.login(externalId, profile)` is a Kotlin `suspend` function. From Java, pass `Continue.with` as the trailing argument instead of calling it on an executor. It requires Android API level 24 or later, and its callback may not run on the main thread. ```java public class OneSignalManager { private final ExecutorService executor = Executors.newSingleThreadExecutor(); public void initialize(Context context, String appId) { executor.execute(() -> { // Set log level for debugging (remove in production) OneSignal.getDebug().setLogLevel(LogLevel.VERBOSE); // Initialize OneSignal OneSignal.initWithContext(context, appId); }); } public void login(String externalId, OneSignalUserProfile profile) { OneSignal.login(externalId, profile, Continue.with(continueResult -> { if (!continueResult.isSuccess()) { Log.w("OneSignal", "Login threw", continueResult.getThrowable()); return; } OneSignalResult result = continueResult.getData(); if (!result.isSuccess()) { Log.w("OneSignal", "Login failed: " + result.getError().getFirst().getMessage()); } })); } } ``` --- ## Code Examples ### Dependency (build.gradle.kts) ```kotlin dependencies { implementation("com.onesignal:OneSignal:[5.0.0, 5.99.99]") } ``` ### Dependency (build.gradle) ```groovy dependencies { implementation 'com.onesignal:OneSignal:[5.0.0, 5.99.99]' } ``` `login(externalId, profile)` was added in 5.11.0. If the project pins an older version, either raise the pin or fall back to `login(externalId)` plus `addEmail()` / `addSms()` / `addTag()`. ### Identifying users: use `login(externalId, profile)` On SDK 5.11.0 and later this is the method to reach for. It sends the external ID together with the user's email, SMS number, tags, and aliases as a single Create User request, and it returns a `OneSignalResult` so the caller can see whether it worked. Build the profile with `OneSignalUserProfile.builder()`; every field is optional. Use identity-only `login(externalId)` only when you have nothing but the user's ID at sign-in, then add the rest with `addEmail()` / `addSms()` / `addTag()` afterward. Rules that matter when writing the call: * It is a `suspend` function. Call it from a coroutine in Kotlin, or pass `Continue.with` in Java. * `setSms()` requires E.164 with the leading `+` (for example `+15551234567`). A malformed number returns `INVALID_ARGUMENT` and sends no request. * `setEmailEnabled(true)` / `setSmsEnabled(true)` subscribe that address, and `false` unsubscribes it. Leave the flag unset to send no status, which subscribes a new address and leaves an existing address's status unchanged. Setting the flag is the only way to resubscribe an address that previously opted out, which `addEmail()` and `addSms()` cannot do. * **Do not key the flag off whether you have the address.** Having a user's email is not consent, and `true` resubscribes someone who opted out. Set it only from a real signal, such as a checkbox the user just ticked. If the app has no such signal, leave it unset. * Do not pass an email or phone number the user typed without verifying it. An address that already belongs to another user moves to whoever logs in. Pair it with Identity Verification. * Ordering no longer matters for anything in the profile, because it ships inside the login request. The "login before tags/email/sms" rule still applies to the granular methods. ### Centralized Manager (Kotlin) Create a singleton manager that wraps all OneSignal SDK calls: ```kotlin @Singleton class OneSignalManager @Inject constructor( @ApplicationContext private val context: Context ) { private var isInitialized = false suspend fun initialize(appId: String) = withContext(Dispatchers.IO) { if (isInitialized) return@withContext // Set log level for debugging (remove in production) OneSignal.Debug.logLevel = LogLevel.VERBOSE // Initialize OneSignal OneSignal.initWithContext(context, appId) isInitialized = true } /** * Identifies the user and sets their email, SMS, and tags in one request. * Pass only what you have; every profile field is optional. * * Leave [emailSubscribed] / [smsSubscribed] null unless the user actually * opted in or out. Passing true resubscribes an address that opted out, so * having the address is not a reason to send a status. */ suspend fun login( externalId: String, email: String? = null, smsE164: String? = null, tags: Map = emptyMap(), emailSubscribed: Boolean? = null, smsSubscribed: Boolean? = null ): OneSignalResult = withContext(Dispatchers.IO) { val profile = OneSignalUserProfile.builder() .setEmail(email) .setSms(smsE164) .setTags(tags) .apply { // Set a status only where the user actually expressed intent. // Null leaves an existing address's status untouched. emailSubscribed?.let { setEmailEnabled(it) } smsSubscribed?.let { setSmsEnabled(it) } } .build() OneSignal.login(externalId, profile) } suspend fun logout() = withContext(Dispatchers.IO) { OneSignal.logout() } /** Use when the user supplies an address after sign-in. Always sends it subscribed. */ suspend fun setEmail(email: String) = withContext(Dispatchers.IO) { OneSignal.User.addEmail(email) } suspend fun setSmsNumber(number: String) = withContext(Dispatchers.IO) { OneSignal.User.addSms(number) } suspend fun setTag(key: String, value: String) = withContext(Dispatchers.IO) { OneSignal.User.addTag(key, value) } fun setLogLevel(level: LogLevel) { OneSignal.Debug.logLevel = level } } ``` ### ViewModel (Kotlin) Initialize OneSignal from a ViewModel on a background thread: ```kotlin @HiltViewModel class MainViewModel @Inject constructor( private val oneSignalManager: OneSignalManager ) : ViewModel() { init { initializeOneSignal() } private fun initializeOneSignal() { viewModelScope.launch { // Runs on background thread via Dispatchers.IO in manager oneSignalManager.setLogLevel(LogLevel.VERBOSE) // Debug only oneSignalManager.initialize("YOUR_ONESIGNAL_APP_ID") } } // emailSubscribed comes from a real opt-in signal, not from having the address. fun login(externalId: String, email: String?, smsE164: String?, emailSubscribed: Boolean?) { viewModelScope.launch { val result = oneSignalManager.login(externalId, email, smsE164, emailSubscribed = emailSubscribed) if (!result.isSuccess) { // The device is on the new external ID either way, so report and move on. Log.w("OneSignal", "Login failed: ${result.error?.first?.message}") } } } fun logout() { viewModelScope.launch { oneSignalManager.logout() } } } ``` ### Application Class (Kotlin) Keep the Application class minimal - just set up Hilt: ```kotlin @HiltAndroidApp class MyApplication : Application() ``` --- ## Push Subscription Verification Dialog After completing SDK initialization, add a push subscription observer so the app can confirm that the device registered successfully. The verification flow is: 1. Register a push subscription observer, and also check the current subscription ID immediately — on SDK 5.x the ID can already be assigned before an `Activity` attaches the observer, so reacting only to the change event would miss the transition 2. Treat the device as registered only when the subscription ID is a real, server-assigned value: non-empty and **not** prefixed with `local-` (the `local-` ID is a pre-registration placeholder the SDK assigns during `initWithContext`) 3. When registered, show the native dialog exactly once (guarded by a "shown once" flag): - Title: "Your OneSignal SDK integration is complete!" - Message: "You can now send Push Notifications & In-App Messages through OneSignal. Tap below to enable push notifications." - Button: "Got it" 4. On button tap, request push permission 5. If permission is granted, no additional action is required Do **not** clear app data (`adb shell pm clear …`), wipe the emulator, or uninstall/reinstall just to re-check — that creates a second OneSignal subscription. Rebuild/relaunch while preserving app storage. See shared guidelines: **Verification without duplicate subscriptions**. ### Kotlin ```kotlin import android.content.Context import android.os.Handler import android.os.Looper import androidx.appcompat.app.AlertDialog import com.onesignal.OneSignal import com.onesignal.user.subscriptions.IPushSubscriptionObserver import com.onesignal.user.subscriptions.PushSubscriptionChangedState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean // Ensures the dialog is shown exactly once private val dialogShown = AtomicBoolean(false) // OneSignal stores observers weakly — keep a strong reference for the screen/app lifetime. private var pushSubscriptionObserver: IPushSubscriptionObserver? = null // A real, server-assigned subscription ID is non-empty and not the local- placeholder private fun isRegistered(subscriptionId: String?): Boolean = !subscriptionId.isNullOrEmpty() && !subscriptionId.startsWith("local-") private fun maybeShowIntegrationCompleteDialog(context: Context, subscriptionId: String?) { if (isRegistered(subscriptionId) && dialogShown.compareAndSet(false, true)) { Handler(Looper.getMainLooper()).post { showIntegrationCompleteDialog(context) } } } fun setupPushSubscriptionObserver(context: Context) { val observer = object : IPushSubscriptionObserver { override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { maybeShowIntegrationCompleteDialog(context, state.current.id) } } pushSubscriptionObserver = observer OneSignal.User.pushSubscription.addObserver(observer) // The ID may already be server-assigned before the observer attaches, // so evaluate the current value immediately as well. maybeShowIntegrationCompleteDialog(context, OneSignal.User.pushSubscription.id) } fun showIntegrationCompleteDialog(context: Context) { AlertDialog.Builder(context) .setTitle("Your OneSignal SDK integration is complete!") .setMessage( "You can now send Push Notifications & In-App Messages through OneSignal. " + "Tap below to enable push notifications." ) .setPositiveButton("Got it") { _, _ -> requestPushPermission() } .setCancelable(false) .show() } fun requestPushPermission() { CoroutineScope(Dispatchers.Main).launch { OneSignal.Notifications.requestPermission(true) } } ``` ### Java ```java import android.content.Context; import android.os.Handler; import android.os.Looper; import androidx.appcompat.app.AlertDialog; import com.onesignal.OneSignal; import com.onesignal.user.subscriptions.IPushSubscriptionObserver; import com.onesignal.user.subscriptions.PushSubscriptionChangedState; import java.util.concurrent.atomic.AtomicBoolean; // Ensures the dialog is shown exactly once private static final AtomicBoolean dialogShown = new AtomicBoolean(false); // OneSignal stores observers weakly — keep a strong reference for the screen/app lifetime. private static IPushSubscriptionObserver pushSubscriptionObserver; // A real, server-assigned subscription ID is non-empty and not the local- placeholder private static boolean isRegistered(String subscriptionId) { return subscriptionId != null && !subscriptionId.isEmpty() && !subscriptionId.startsWith("local-"); } private static void maybeShowIntegrationCompleteDialog(Context context, String subscriptionId) { if (isRegistered(subscriptionId) && dialogShown.compareAndSet(false, true)) { new Handler(Looper.getMainLooper()).post(() -> { showIntegrationCompleteDialog(context); }); } } public static void setupPushSubscriptionObserver(Context context) { IPushSubscriptionObserver observer = new IPushSubscriptionObserver() { @Override public void onPushSubscriptionChange(PushSubscriptionChangedState state) { maybeShowIntegrationCompleteDialog(context, state.getCurrent().getId()); } }; pushSubscriptionObserver = observer; OneSignal.getUser().getPushSubscription().addObserver(observer); // The ID may already be server-assigned before the observer attaches, // so evaluate the current value immediately as well. maybeShowIntegrationCompleteDialog(context, OneSignal.getUser().getPushSubscription().getId()); } public static void showIntegrationCompleteDialog(Context context) { new AlertDialog.Builder(context) .setTitle("Your OneSignal SDK integration is complete!") .setMessage( "You can now send Push Notifications & In-App Messages through OneSignal. " + "Tap below to enable push notifications." ) .setPositiveButton("Got it", (dialog, which) -> { OneSignal.getNotifications().requestPermission(true, result -> {}); }) .setCancelable(false) .show(); } ``` --- ## Troubleshooting | Issue | Solution | |-------|----------| | Push not received | Check notification permission and that the App ID matches the project; confirm internet connectivity | | Duplicate subscriptions after re-test | Do not clear app data or uninstall/reinstall to re-verify — relaunch preserving storage (see shared guidelines) | | Permission denied | Ensure `POST_NOTIFICATIONS` is requested on Android 13+ | | `ActivityNotFoundException` for `PermissionsActivity` | Open the app `AndroidManifest.xml`. If `` has `tools:node="replace"`, warn the user: that drops OneSignal's `PermissionsActivity` and crashes the notification permission flow. Remove `tools:node="replace"`. Override one attribute with `tools:replace` instead. Confirm `com.onesignal.core.activities.PermissionsActivity` is in the merged manifest. | | Initialization failed | Verify App ID is correct and internet permission is granted | | ProGuard issues | Check OneSignal rules are not being stripped |