Geliştiriciler · API v1.0.0

Ionic (Capacitor) Entegrasyonu

Bu rehber Ionic + Capacitor ile geliştirilen Android ve iOS uygulamaları içindir (Angular, React veya Vue fark etmez). Genel API referansı için Genel bakış.

Desteklenen sürümler

Bileşen Minimum Önerilen
Capacitor 6 güncel
@capacitor/push-notifications 6.x güncel
Ionic Framework 7 güncel
Android 6.0 (API 23) targetSdk 35+
iOS 13.0 15.0+

Cordova tabanlı eski Ionic projeleri için resmi destek yoktur; Capacitor'a geçiş önerilir. PWA/web push 1.0.0'da desteklenmez.

Nasıl çalışır?

NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder. Uygulamanız:

  1. @capacitor/push-notifications ile native token'ı alır (Android: FCM token, iOS: APNs token),
  2. Bu token'ı Expo token servisinde bir Expo Push Token'a dönüştürür,
  3. Expo Push Token'ı NotiPilot'a register-device ile kaydeder.

1. Ön hazırlık (bir kez)

  1. Android: Firebase konsolunda Android uygulamanızı ekleyin, google-services.json dosyasını android/app/ içine koyun.
  2. iOS: Apple Developer'da Push Notifications capability'sini açın ve bir APNs .p8 anahtarı oluşturun (adımlar: iOS rehberi).
  3. expo.dev üzerinde bir proje oluşturun, proje ID'sini not edin ve Credentials bölümüne FCM V1 servis hesabı JSON'unu (Android) ve APNs .p8 anahtarını (iOS) yükleyin.
  4. NotiPilot panelinde uygulamanızı ekleyin: Expo Project ID, Expo Access Token, Android Package, iOS Bundle ID (= capacitor.config.ts içindeki appId).

2. Kurulum

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

iOS: Xcode'da (npx cap open ios) App target → Signing & Capabilities → Push Notifications ve Background Modes → Remote notifications ekleyin. ios/App/App/AppDelegate.swift içine şunları ekleyin:

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: android/variables.gradle içinde minSdkVersion = 23 olmalıdır.

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 = 'EXPO-PROJE-ID-NIZ'; // Paneldeki "Expo Project ID"
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 build'leri APNs sandbox kullanır
      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;
}

/** Uygulama açılışında çağırın. */
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,                // örn. { city: 'Istanbul' }
        },
      });
    } catch (e) {
      console.warn('[NotiPilot] register failed', e);
    }
  });

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

  // Bildirime tıklandı — panelden gönderilen özel veri
  await PushNotifications.addListener('pushNotificationActionPerformed', (action: ActionPerformed) => {
    const raw = action.notification.data?.body;
    const data = typeof raw === 'string' ? safeJson(raw) : raw ?? action.notification.data;
    // örn. data.screen === 'product' → router.navigate(...)
  });

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

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

/** Kullanıcı giriş yaptığında çağırın. */
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 Vite içindir. Angular kullanıyorsanız !environment.production, Webpack tabanlı projelerde process.env.NODE_ENV !== 'production' kullanın.

PushNotifications.register() her uygulama açılışında çağrıldığında registration olayı yeniden tetiklenir; bu sayede token değişiklikleri otomatik olarak NotiPilot'a iletilir.

4. Kullanım

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' }));

Kullanıcı giriş yaptığında: await identify(user.id);

Kontrol listesi

  • google-services.json eklendi (Android), Push capability'leri açık (iOS)
  • iOS AppDelegate.swift içine Capacitor push metotları eklendi
  • FCM V1 ve APNs kimlik bilgileri Expo projesine yüklendi
  • Paneldeki Expo Project ID = APP_ID, Bundle ID / Package = capacitor.config.ts → appId
  • country ve mümkünse city attributes olarak gönderiliyor

Sık karşılaşılan sorunlar

Belirti Çözüm
registration olayı hiç gelmiyor (iOS) AppDelegate.swift metotları eksik veya capability kapalı.
Android'de uygulama çöküyor google-services.json eksik.
Token dönüşümü 4xx dönüyor projectId ve appId (bundle ID) değerlerini kontrol edin.
404 unknown_app app_id panelde kayıtlı değil.