PushApp SDK

July 3, 2026 · View on GitHub

Step-by-step integration manual for PushApp Ionic/Capacitor SDK — push notifications, in-app messaging, and event tracking.

Navigation hub: README.md (when to call what, API summary, troubleshooting index)

Canonical code: example-app/ — copy pushapp-setup.ts first

Supported platforms: Android and iOS (Ionic / Angular / Capacitor). Not supported in the browser.


1. Before you start

What you need from PushApp

ItemDescription
App IDYour channel id, e.g. yourtenant_1234567890. The part before the first _ is your tenant name.
EnvironmentProduction (sandbox: false{tenant}.pushapp.ai) or sandbox (sandbox: true{tenant}.pushapp.co.in).
Firebase projectYou configure Firebase in your own app; PushApp uses it for push delivery.

Your app requirements

  • Ionic + Capacitor project (Capacitor 7+ required)
  • Node.js 18+
  • Android Studio (Android) and/or Xcode (iOS)
  • Firebase project with Android and/or iOS apps configured
  • Real device for push testing (emulators are unreliable for FCM/APNs)

Store config in environment.ts

// src/environments/environment.ts
export const environment = {
  production: false,
  pushApp: {
    appId: 'yourtenant_1234567890',
    sandbox: true,
    debugMode: true, // dev only
  },
};

2. Install the SDK

npm install pushapp-ionic @capacitor/push-notifications
npx cap sync

For iOS:

cd ios/App && pod install

@capacitor/push-notifications is a peer dependency. The example app uses the SDK's native token caching + retry pattern instead — see §5B.


3. Firebase setup

Android

  1. In Firebase Console, add an Android app with your package name (must match applicationId in android/app/build.gradle).
  2. Download google-services.json.
  3. Place it at: android/app/google-services.json
  4. Complete §3b Android Gradle — the JSON file alone is not enough.
  5. Rebuild: npx cap sync android

iOS

  1. In Firebase Console, add an iOS app with your bundle id.
  2. Download GoogleService-Info.plist.
  3. Add it to your Xcode app target (ios/App/App/).
  4. In Xcode, enable Push Notifications capability for your app target.
  5. Add Firebase Messaging to ios/App/Podfile:
platform :ios, '15.2'  # SDK minimum

target 'App' do
  # ... Capacitor pods ...
  pod 'Firebase/Messaging'
end
  1. Run pod install in ios/App/, then npx cap sync ios.

Reference: example-app/ios/App/Podfile.

3b. Android Gradle (required)

google-services.json alone does not enable FCM. You must apply the Google Services Gradle plugin.

1. Project-level android/build.gradle — add the classpath:

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

2. App-level android/app/build.gradle — apply the plugin when the file exists:

try {
    def servicesJSON = file('google-services.json')
    if (servicesJSON.text) {
        apply plugin: 'com.google.gms.google-services'
    }
} catch(Exception e) {
    logger.info("google-services.json not found, google-services plugin not applied.")
}

3. Rebuild:

npx cap sync android

Reference: example-app/android/build.gradle and example-app/android/app/build.gradle.

4. Optional but recommended — in android/app/src/main/AndroidManifest.xml inside <application>:

<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@mipmap/ic_launcher" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="pushapp_channel_id" />

4. iOS native setup

Update ios/App/App/AppDelegate.swift so push notifications work, Firebase receives the APNs token, and the SDK receives both APNs and FCM tokens.

import UserNotifications
import PushappIonic
import Firebase
import FirebaseMessaging

// AppDelegate must conform to UNUserNotificationCenterDelegate and MessagingDelegate
class AppDelegate: UIResponder, UIApplicationDelegate,
                     UNUserNotificationCenterDelegate, MessagingDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        Messaging.messaging().delegate = self
        UNUserNotificationCenter.current().delegate = self

        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
        return true
    }

    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
        if #available(iOS 15.2, *) {
            PushApp.shared.handleDeviceToken(deviceToken)
        }
    }

    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let fcmToken = fcmToken, !fcmToken.isEmpty else { return }
        if #available(iOS 15.2, *) {
            PushApp.shared.handleFcmTokenRefresh(fcmToken)
        }
    }

    // Show notifications when app is in foreground
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        if #available(iOS 14.0, *) {
            completionHandler([.banner, .sound, .badge])
        } else {
            completionHandler([.alert, .sound, .badge])
        }
    }
}

Minimum iOS version for the SDK: 15.2

Full working example (including notification tap tracking): example-app/ios/App/App/AppDelegate.swift

4b. iOS notification open / CTA tracking

When the user taps a push notification, call the SDK so campaigns are tracked. Without this, trackPushNotificationEvent never fires.

Add to AppDelegate.swift:

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo
    let actionId = response.actionIdentifier

    guard let token = userInfo["click_token"] as? String else {
        completionHandler()
        return
    }

    var event = "opened"
    var ctaId: String? = nil

    // Handle CTA button taps
    if let buttons = userInfo["buttons"] as? [[String: Any]],
       let matchingButton = buttons.first(where: { \$0["id"] as? String == actionId }) {
        event = "cta"
        ctaId = matchingButton["id"] as? String
        if let urlString = matchingButton["url"] as? String,
           let url = URL(string: urlString) {
            UIApplication.shared.open(url)
        }
    }

    if #available(iOS 15.2, *) {
        PushApp.shared.trackPushNotificationEvent(token: token, event: event, ctaId: ctaId) { _ in }
    }
    completionHandler()
}

Full working example: example-app/ios/App/App/AppDelegate.swift

Android: notification tap tracking is handled by the SDK's NotificationClickReceiver — no extra app code required.


5. SDK integration (required)

Call order

Always call SDK methods in this order:

initialize()  →  register()  →  login()

If the order is wrong, the SDK logs an error and rejects the promise. The app will not crash.

File placement (Ionic Angular)

SDK callSuggested fileLifecycle hook
initialize + registerapp.component.ts or pushapp-setup.tsngOnInit
login + saveUserDatalogin.page.tsAfter auth success
logoutLogout handler / home.page.tsUser taps sign out
setPageName / sendEventEach page .tsionViewDidEnter
registerPlaceholderPage with DOM slotionViewDidEnter / ionViewWillLeave
registerTooltipTargetPage with anchor elementionViewDidEnter / ionViewWillLeave
AppDelegate changesios/App/App/AppDelegate.swiftNative — one-time setup

Flow A — App launch (every cold start)

Call initialize() and register() as early as possible — on the login screen or in app.component.tsbefore the user signs in.

// pushapp-setup.ts (recommended — copy from example-app)
import { Capacitor } from '@capacitor/core';
import { PushApp } from 'pushapp-ionic';
import { environment } from '../environments/environment';

export async function setupPushAppOnLaunch(): Promise<void> {
  if (!Capacitor.isNativePlatform()) return;

  await PushApp.initialize({
    appId: environment.pushApp.appId,
    pushAppId: environment.pushApp.pushAppId,
    appSecretKey: environment.pushApp.appSecretKey,
    sandbox: environment.pushApp.sandbox,
    debugMode: !environment.production && environment.pushApp.debugMode,
  });

  await registerDevice(); // see §5B
}
// app.component.ts or login.page.ts — ngOnInit
import { setupPushAppOnLaunch } from './pushapp-setup';

await setupPushAppOnLaunch();

Flow B — After user authenticates

Call login() and saveUserData() in your login success handler — not at app startup.

// login.page.ts — on successful auth
import { PushApp } from 'pushapp-ionic';

const res = await PushApp.login({ userId: this.username });

const headers = await PushApp.getDeviceHeaders();
const deviceId = headers['X-Device-ID'] ?? '';
const code = `${this.username}_${deviceId}`;

await PushApp.saveUserData({
  code,
  additionalInfo: { /* your fields */ },
  cohorts: { /* your segments */ },
});

Flow C — Returning user (session restore)

If the user is already logged in (e.g. token in localStorage), run the launch flow and call login() again with the stored user id.

// app.component.ts — ngOnInit
const userId = localStorage.getItem('username');
if (userId) {
  await setupPushAppOnLaunch();
  await PushApp.login({ userId });
  navigateToHome();
} else {
  navigateToLogin();
}

On the login page, still call setupPushAppOnLaunch() so initialize + register run before the user signs in.


Step A — Initialize

await PushApp.initialize({
  appId: environment.pushApp.appId,
  pushAppId: environment.pushApp.pushAppId,
  appSecretKey: environment.pushApp.appSecretKey,
  sandbox: environment.pushApp.sandbox,
  debugMode: environment.pushApp.debugMode, // optional — verbose logs in debug builds
  // slackWebhookUrl: '...'  // dev/integration only — never in production
});

5B — Register patterns (pick one)

PatternWhen to useCode
A — Native cached token (recommended)Ionic apps using this SDKPushApp.register({ fcmToken: '' }) with retry — Android and iOS — see example-app/src/app/pushapp-setup.ts
B — Capacitor listenerWhen you already use @capacitor/push-notifications listenersListener passes token.value to register()

The native SDK caches the FCM/APNs token. On first launch, tokens may not be ready yet — retry until they arrive (Android and iOS):

const REGISTER_RETRY_MS = 1000;
const REGISTER_MAX_ATTEMPTS = 8;

export async function registerDevice(fcmToken?: string): Promise<void> {
  for (let attempt = 1; attempt <= REGISTER_MAX_ATTEMPTS; attempt++) {
    try {
      await PushApp.register({ fcmToken: fcmToken ?? '' });
      return;
    } catch (err) {
      if (attempt < REGISTER_MAX_ATTEMPTS) {
        await new Promise((r) => setTimeout(r, REGISTER_RETRY_MS));
      } else {
        throw err;
      }
    }
  }
}

register() is safe on every app open. The SDK:

  • Avoids duplicate registration when already registered with the same push token
  • Updates the stored push token automatically when it changes after a successful register
  • After logout(), clears local registration state and registers again as a fresh guest device

No extra JavaScript is required for token refresh after the first successful register.

iOS note

With Pattern A, AppDelegate must wire Firebase + APNs as in §4: handleDeviceToken, handleFcmTokenRefresh, and Messaging.messaging().apnsToken. You do not need to pass apnsToken from JavaScript — the plugin reads native cached tokens when fcmToken is empty.

Pattern B — Capacitor Push Notifications

import { PushNotifications } from '@capacitor/push-notifications';

await PushNotifications.requestPermissions();
await PushNotifications.register();

PushNotifications.addListener('registration', async (token) => {
  await PushApp.register({ fcmToken: token.value });
});

On iOS with Pattern B, pass the hex APNs token from your listener:

await PushApp.register({ apnsToken: apnsTokenHex, fcmToken: fcmTokenOptional });

Step C — Login (after user signs in)

await PushApp.login({ userId: 'USER_ID' });

Use your own user id (email, internal id, etc.) — the same id you use in your backend.

Step D — Logout (on sign-out)

try {
  await PushApp.logout(); // clears local session + server delink
} catch (err) {
  // LOGOUT_FAILED — local session is still cleared
  console.warn('PushApp.logout:', err);
}
clearLocalAuthState();
navigateToLogin();

Call logout() before wiping local storage. If server delink fails, local session is still cleared (LOGOUT_FAILED may be returned).

Example: example-app/src/app/home/home.page.tslogout() method.


Send user profile and segmentation data after login:

const headers = await PushApp.getDeviceHeaders();
const deviceId = headers['X-Device-ID'] ?? '';
const code = `${userId}_${deviceId}`;

await PushApp.saveUserData({
  code,
  additionalInfo: {
    city: 'Mumbai',
    plan: 'premium',
  },
  cohorts: {
    segment: 'active_user',
  },
});

Profiles are not updated automatically on app open — call saveUserData() explicitly after login.


7. Event and page tracking

Track page views

Call when the user navigates to a screen (ionViewDidEnter):

await PushApp.setPageName({ pageName: 'home' });

Track custom events

await PushApp.sendEvent({
  eventName: 'button_clicked',
  eventData: {
    source: 'checkout',
    method: 'card',
  },
});

8. Optional — In-app message placements

Use these only if PushApp campaigns include inline or tooltip messages.

Inline placeholder

The SDK automatically tracks placeholder position on scroll, resize, and fixed headers. Your app only registers and unregisters.

  1. Add a container in your HTML — the element id must match placeholderId:
<div id="promo-banner" class="promo-slot"></div>
  1. Register when the view loads; unregister when leaving:
// ionViewDidEnter
await PushApp.registerPlaceholder({ placeholderId: 'promo-banner' });

// optional: different DOM id or custom fixed header selector
await PushApp.registerPlaceholder({
  placeholderId: 'promo_banner',      // campaign id
  elementId: 'promo-banner',          // HTML id if different
  clipTopSelector: 'ion-header',      // default; clips below fixed chrome
});

// ionViewWillLeave
await PushApp.unregisterPlaceholder({ placeholderId: 'promo-banner' });

Notes:

  • placeholderId must match the PushApp campaign inline slot id.
  • Scroll sync and repositioning are handled inside the SDK — no updatePlaceholder calls in app code.
  • Guard with Capacitor.isNativePlatform() — placeholders are not supported in the browser.

Tooltip target

Register anchor elements for tooltip/popover campaigns. Register after DOM layout is complete.

<ion-fab-button id="tooltip-target">...</ion-fab-button>
ionViewDidEnter() {
  if (!Capacitor.isNativePlatform()) return;

  requestAnimationFrame(() => {
    setTimeout(() => this.registerTooltipTarget(), 250);
  });
}

async registerTooltipTarget() {
  const el = document.getElementById('tooltip-target');
  if (!el) return;

  const rect = el.getBoundingClientRect();

  await PushApp.registerTooltipTarget({
    targetId: 'center', // must match PushApp campaign config
    x: Math.round(rect.left),
    y: Math.round(rect.top),
    width: Math.round(rect.width),
    height: Math.round(rect.height),
  });
}

ionViewWillLeave() {
  PushApp.unregisterTooltipTarget({ targetId: 'center' }).catch(() => undefined);
}

Reference: example-app/src/app/home/home.page.ts


9. Integration checklist

Use this before going live:

  • google-services.json (Android) and/or GoogleService-Info.plist (iOS) added
  • Android google-services Gradle plugin applied (§3b)
  • iOS Push Notifications capability enabled; pod 'Firebase/Messaging' in Podfile
  • AppDelegate wires Firebase, APNs, and FCM refresh (§4)
  • iOS notification tap handler wired (§4b)
  • PushApp.initialize() called at app startup with channel appId, pushAppId, and appSecretKey
  • PushApp.register() called after initialize (retry pattern — Android and iOS)
  • PushApp.login() called after user authentication
  • PushApp.logout() called on sign-out
  • saveUserData() called after login (if using profiles/segments)
  • setPageName() called on main screens
  • Inline placeholders: HTML id matches placeholderId; register on enter, unregister on leave
  • Tested on a real device (push does not work in browser or reliably on emulators)

10. Troubleshooting

IssueWhat to check
initialize failsApp ID format must be tenant_suffix (e.g. demo_1763369170735). Check Logcat (Android) or Xcode console (iOS).
register rejected / EMPTY_TOKENCall initialize() first. On Android first launch, token may not be ready — use retry pattern from pushapp-setup.ts.
login rejected / REGISTER_REQUIREDCall register() successfully before login().
No push on deviceReal device required. Check Firebase config, Gradle plugin (§3b), and notification permissions (Android 13+: tap Allow when prompted).
Testing in browserExpected: WEB_NOT_SUPPORTED — use a device.
After logout, push tied to old userCall logout() before clearing local auth.
iOS token not receivedVerify AppDelegate forwards token via PushApp.shared.handleDeviceToken().
Push tap not tracked (iOS)Implement §4b.
Inline never appearsplaceholderId must match campaign + HTML id; register after view loads; call setPageName / sendEvent.
Inline overlaps headerDefault clipTopSelector: 'ion-header' clips below fixed chrome; adjust if your header uses a custom selector.

Android logs: adb logcat -s PushApp:D MySdk:D
iOS logs: Xcode console → filter PushApp


11. API reference

Method signatures and options are maintained in the README API table. Auto-generated details: run npm run docgenapi-reference.md.


12. Lifecycle diagram

sequenceDiagram
    participant App
    participant SDK

    App->>SDK: initialize()
    App->>SDK: register()
    App->>SDK: login(userId)
    App->>SDK: saveUserData()
    App->>SDK: setPageName() / sendEvent()
    Note over App,SDK: On sign-out
    App->>SDK: logout()

13. Error handling

Native methods reject with stable code values. Use getPushAppErrorCode(err) from pushapp-ionic — do not parse error message strings.

import { PushApp, PushAppErrorCode, getPushAppErrorCode } from 'pushapp-ionic';

try {
  await PushApp.register({ fcmToken: '' });
} catch (err) {
  switch (getPushAppErrorCode(err)) {
    case PushAppErrorCode.EMPTY_TOKEN:
      // retry — see pushapp-setup.ts
      break;
    case PushAppErrorCode.NOT_INITIALIZED:
      await PushApp.initialize({
        appId: '...',
        pushAppId: 'pa_...',
        appSecretKey: 'pas_...',
      });
      break;
  }
}
CodeMeaningAction
NOT_INITIALIZEDinitialize() not calledCall initialize() first
EMPTY_TOKENNo push token yetRetry register() — see pushapp-setup.ts
REGISTER_REQUIREDlogin() before register()Call register() first
REGISTER_FAILEDRegister API failedCheck network, App ID, Firebase config
LOGIN_FAILEDLogin API failedEnsure register() succeeded first
LOGOUT_FAILEDServer delink failedLocal session still cleared — safe to proceed
WEB_NOT_SUPPORTEDRunning in browserUse a real device
INVALID_APP_IDApp ID format wrongUse tenant_suffix format

Full list: src/errors.ts in the SDK package.


14. Support


Document version: 2.0 — PushApp Ionic SDK 0.1.2