Entwickler · API v1.0.0

Ionic-Integration (Capacitor)

Dieser Leitfaden richtet sich an Android- und iOS-Apps, die mit Ionic + Capacitor entwickelt werden (egal ob mit Angular, React oder Vue). Die allgemeine API-Referenz finden Sie in der Überblick.

Unterstützte Versionen

Komponente Minimum Empfohlen
Capacitor 6 aktuell
@capacitor/push-notifications 6.x aktuell
Ionic Framework 7 aktuell
Android 6.0 (API 23) targetSdk 35+
iOS 13.0 15.0+

Ältere, Cordova-basierte Ionic-Projekte werden nicht offiziell unterstützt; ein Umstieg auf Capacitor wird empfohlen. PWA/Web Push wird in Version 1.0.0 nicht unterstützt.

Wie funktioniert es?

NotiPilot 1.0.0 stellt Benachrichtigungen über den Expo Push Service zu. Ihre App:

  1. ruft mit @capacitor/push-notifications den nativen Token ab (Android: FCM-Token, iOS: APNs-Token),
  2. konvertiert diesen Token über den Expo-Token-Service in einen Expo Push Token,
  3. registriert den Expo Push Token mit register-device bei NotiPilot.

1. Vorbereitung (einmalig)

  1. Android: Fügen Sie Ihre Android-App in der Firebase-Konsole hinzu und legen Sie die Datei google-services.json in android/app/ ab.
  2. iOS: Aktivieren Sie im Apple Developer Portal die Capability Push Notifications und erstellen Sie einen APNs-Schlüssel (.p8) (Schritte: iOS-Leitfaden).
  3. Legen Sie auf expo.dev ein Projekt an, notieren Sie die Projekt-ID und laden Sie im Bereich Credentials das JSON des FCM-V1-Dienstkontos (Android) sowie den APNs-Schlüssel (.p8) (iOS) hoch.
  4. Legen Sie Ihre App im NotiPilot-Dashboard an: Expo Project ID, Expo Access Token, Android Package, iOS Bundle ID (= appId in capacitor.config.ts).

2. Installation

Terminal
npm install @capacitor/push-notifications @capacitor/preferences @capacitor/app @capacitor/device
npx cap sync

iOS: Fügen Sie in Xcode (npx cap open ios) unter App-Target → Signing & Capabilities die Capabilities Push Notifications und Background Modes → Remote notifications hinzu. Ergänzen Sie in ios/App/App/AppDelegate.swift Folgendes:

Swift
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}

Android: In android/variables.gradle muss minSdkVersion = 23 gesetzt sein.

capacitor.config.ts:

TypeScript
const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Example',
  webDir: 'dist',
  plugins: {
    PushNotifications: { presentationOptions: ['badge', 'sound', 'alert'] },
  },
};

3. src/notipilot.ts

TypeScript
import { Capacitor } from '@capacitor/core';
import { PushNotifications, type Token, type ActionPerformed } from '@capacitor/push-notifications';
import { Preferences } from '@capacitor/preferences';
import { App } from '@capacitor/app';
import { Device } from '@capacitor/device';

const NOTIPILOT_BASE_URL = 'https://app.notipilot.com/api/v1';
const APP_ID = 'IHRE-EXPO-PROJEKT-ID'; // "Expo Project ID" aus dem Dashboard
const UID_KEY = 'notipilot_device_uid';

type Attributes = Record<string, string | number | boolean | null>;

async function deviceUid(): Promise<string> {
  const { value } = await Preferences.get({ key: UID_KEY });
  if (value) return value;
  const uid = crypto.randomUUID();
  await Preferences.set({ key: UID_KEY, value: uid });
  return uid;
}

async function post(path: string, body: object, attempt = 0): Promise<any> {
  const res = await fetch(`${NOTIPILOT_BASE_URL}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json().catch(() => ({}));
  if ((res.status === 429 || res.status >= 500) && attempt < 3) {
    await new Promise((r) => setTimeout(r, (Number(json.retry_after) || 2 ** attempt) * 1000));
    return post(path, body, attempt + 1);
  }
  if (!res.ok) console.warn('[NotiPilot]', res.status, json.error, json.errors ?? json.message);
  return json;
}

async function toExpoPushToken(nativeToken: string, uid: string): Promise<string> {
  const isIOS = Capacitor.getPlatform() === 'ios';
  const { id: bundleId } = await App.getInfo();

  const res = await fetch('https://exp.host/--/api/v2/push/getExpoPushToken', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      type: isIOS ? 'apns' : 'fcm',
      deviceId: uid.toLowerCase(),
      development: isIOS && import.meta.env.DEV, // iOS-Debug-Builds nutzen die APNs-Sandbox
      appId: bundleId,
      deviceToken: nativeToken,
      projectId: APP_ID,
    }),
  });
  if (!res.ok) throw new Error(`Expo token exchange failed: ${res.status}`);
  return (await res.json()).data.expoPushToken;
}

/** Beim App-Start aufrufen. */
export async function initNotiPilot(extraAttributes: Attributes = {}) {
  if (!Capacitor.isNativePlatform()) return;

  await PushNotifications.addListener('registration', async (token: Token) => {
    try {
      const uid = await deviceUid();
      const expoToken = await toExpoPushToken(token.value, uid);
      const { languageCode } = await Device.getLanguageCode();
      const { version } = await App.getInfo();
      const region = new Intl.Locale(navigator.language).region ?? null;

      await post('/register-device', {
        app_id: APP_ID,
        device_uid: uid,
        token: expoToken,
        platform: Capacitor.getPlatform(), // 'ios' | 'android'
        provider: 'expo',
        attributes: {
          locale: languageCode.slice(0, 2),  // "tr"
          country: region,                   // "TR"
          app_version: version,
          ...extraAttributes,                // z. B. { city: 'Istanbul' }
        },
      });
    } catch (e) {
      console.warn('[NotiPilot] register failed', e);
    }
  });

  await PushNotifications.addListener('registrationError', (err) =>
    console.warn('[NotiPilot] native registration error', err),
  );

  // Auf Benachrichtigung getippt — vom Dashboard gesendete benutzerdefinierte Daten
  await PushNotifications.addListener('pushNotificationActionPerformed', (action: ActionPerformed) => {
    const raw = action.notification.data?.body;
    const data = typeof raw === 'string' ? safeJson(raw) : raw ?? action.notification.data;
    // z. B. data.screen === 'product' → router.navigate(...)
  });

  if (Capacitor.getPlatform() === 'android') {
    await PushNotifications.createChannel({ id: 'default', name: 'Allgemein', importance: 5 });
  }

  let perm = await PushNotifications.checkPermissions();
  if (perm.receive === 'prompt') perm = await PushNotifications.requestPermissions();
  if (perm.receive === 'granted') await PushNotifications.register();
}

/** Aufrufen, wenn sich der Nutzer anmeldet. */
export async function identify(externalId: string, attributes?: Attributes) {
  return post('/identify-device', {
    app_id: APP_ID,
    device_uid: await deviceUid(),
    external_id: externalId,
    ...(attributes ? { attributes } : {}),
  });
}

function safeJson(s: string) {
  try { return JSON.parse(s); } catch { return {}; }
}

import.meta.env.DEV gilt für Vite. Verwenden Sie unter Angular !environment.production und in Webpack-basierten Projekten process.env.NODE_ENV !== 'production'.

Wird PushNotifications.register() bei jedem App-Start aufgerufen, wird das Event registration erneut ausgelöst; so werden Token-Änderungen automatisch an NotiPilot übermittelt.

4. Verwendung

Angular (app.component.ts):

TypeScript
constructor() { initNotiPilot({ city: 'Istanbul' }); }

React (App.tsx):

TSX
useEffect(() => { initNotiPilot({ city: 'Istanbul' }); }, []);

Vue (App.vue):

TypeScript
onMounted(() => initNotiPilot({ city: 'Istanbul' }));

Wenn sich der Nutzer anmeldet: await identify(user.id);

Checkliste

  • google-services.json hinzugefügt (Android), Push-Capabilities aktiviert (iOS)
  • Capacitor-Push-Methoden in AppDelegate.swift (iOS) ergänzt
  • FCM-V1- und APNs-Zugangsdaten in das Expo-Projekt hochgeladen
  • Expo Project ID im Dashboard = APP_ID, Bundle ID / Package = capacitor.config.ts → appId
  • country und nach Möglichkeit city werden als Attributes gesendet

Häufige Probleme

Symptom Lösung
Das Event registration wird nie ausgelöst (iOS) Die Methoden in AppDelegate.swift fehlen oder die Capability ist deaktiviert.
App stürzt unter Android ab google-services.json fehlt.
Token-Konvertierung liefert 4xx Prüfen Sie die Werte von projectId und appId (Bundle ID).
404 unknown_app app_id ist im Dashboard nicht registriert.