Geliştiriciler · API v1.0.0

Expo Entegrasyonu

Bu rehber Expo (managed workflow veya development build) ile geliştirilen uygulamalar içindir. Genel API referansı için Genel bakış dosyasına bakın.

Desteklenen sürümler: Expo SDK 50+ (önerilen: güncel SDK) · iOS 13.4+ · Android 7.0+ (API 24)

Uzaktan (remote) push bildirimleri Expo Go içinde çalışmaz (Android'de SDK 53'ten itibaren kaldırıldı). Test için development build veya EAS Build ile üretilmiş bir build kullanın. Push bildirimleri emülatör/simülatörde değil, fiziksel cihazda test edilmelidir.

1. Kurulum

Terminal
npx expo install expo-notifications expo-device expo-constants expo-secure-store expo-localization expo-crypto

app.json / app.config.js:

JSON
{
  "expo": {
    "plugins": [
      ["expo-notifications", { "defaultChannel": "default" }]
    ],
    "extra": {
      "eas": { "projectId": "EAS-PROJE-ID-NIZ" }
    }
  }
}

Push kimlik bilgilerini Expo projenize yükleyin (bir kez):

Terminal
eas credentials   # Android: FCM V1 servis hesabı JSON'u, iOS: APNs key

2. NotiPilot istemcisi

src/notipilot.ts:

TypeScript
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import * as SecureStore from 'expo-secure-store';
import * as Localization from 'expo-localization';
import * as Crypto from 'expo-crypto';
import Constants from 'expo-constants';
import { Platform } from 'react-native';

const NOTIPILOT_BASE_URL = 'https://app.notipilot.com/api/v1';

// NotiPilot panelindeki "Expo Project ID" ile aynı olmalı
const PROJECT_ID: string =
  Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId;

const DEVICE_UID_KEY = 'notipilot_device_uid';

// SDK 52 ve öncesinde shouldShowBanner/shouldShowList yerine shouldShowAlert: true kullanın
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

async function getDeviceUid(): Promise<string> {
  let uid = await SecureStore.getItemAsync(DEVICE_UID_KEY);
  if (!uid) {
    uid = Crypto.randomUUID();
    await SecureStore.setItemAsync(DEVICE_UID_KEY, 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) {
    const waitSec = Number(json.retry_after) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, waitSec * 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 getExpoPushToken(): Promise<string | null> {
  if (!Device.isDevice) return null;

  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'Genel',
      importance: Notifications.AndroidImportance.HIGH,
    });
  }

  let { status } = await Notifications.getPermissionsAsync();
  if (status !== 'granted') {
    ({ status } = await Notifications.requestPermissionsAsync());
  }
  if (status !== 'granted') return null;

  const { data } = await Notifications.getExpoPushTokenAsync({ projectId: PROJECT_ID });
  return data; // "ExponentPushToken[...]"
}

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

/** Uygulama her açıldığında çağırın. */
export async function registerDevice(extra: NotiPilotAttributes = {}) {
  const token = await getExpoPushToken();
  if (!token) return null;

  const locale = Localization.getLocales()[0];

  return post('/register-device', {
    app_id: PROJECT_ID,
    device_uid: await getDeviceUid(),
    token,
    platform: Platform.OS, // 'ios' | 'android'
    provider: 'expo',
    attributes: {
      locale: locale?.languageCode ?? null,   // "tr"
      country: locale?.regionCode ?? null,    // "TR"
      app_version: Constants.expoConfig?.version ?? null,
      ...extra,                               // örn. { city: 'Istanbul' }
    },
  });
}

/** Kullanıcı giriş yaptığında çağırın. */
export async function identify(externalId: string, attributes?: NotiPilotAttributes) {
  return post('/identify-device', {
    app_id: PROJECT_ID,
    device_uid: await getDeviceUid(),
    external_id: externalId,
    ...(attributes ? { attributes } : {}),
  });
}

/** Kullanıcı çıkış yaptığında çağırın. */
export async function logout() {
  const token = await getExpoPushToken();
  if (!token) return null;
  return post('/register-device', {
    app_id: PROJECT_ID,
    device_uid: await getDeviceUid(),
    token,
    platform: Platform.OS,
    external_id: null,
  });
}

/** Token yenilendiğinde NotiPilot'u güncel tutar. Uygulama başlangıcında bir kez çağırın. */
export function listenForTokenChanges() {
  return Notifications.addPushTokenListener(() => {
    registerDevice().catch(() => {});
  });
}

3. Kullanım

TSX
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import { registerDevice, listenForTokenChanges, identify } from './src/notipilot';

export default function App() {
  useEffect(() => {
    registerDevice({ city: 'Istanbul' }).catch(console.warn);
    const tokenSub = listenForTokenChanges();

    // Bildirime tıklanınca panelden gönderilen `data` alanını okuyun
    const tapSub = Notifications.addNotificationResponseReceivedListener((response) => {
      const data = response.notification.request.content.data;
      // örn. data.screen === 'product' → navigation.navigate('Product', { id: data.product_id })
    });

    return () => {
      tokenSub.remove();
      tapSub.remove();
    };
  }, []);

  // Giriş sonrası: await identify(user.id, { gender: user.gender });
  return null;
}

4. Kontrol listesi

  • Paneldeki Expo Project ID = extra.eas.projectId
  • eas credentials ile FCM V1 ve APNs kimlik bilgileri yüklendi
  • Fiziksel cihazda, development/production build ile test edildi
  • country ve mümkünse city gönderiliyor (konum segmentleri için)
  • Giriş sonrası identify, çıkışta logout çağrılıyor

Sık karşılaşılan sorunlar

Belirti Çözüm
404 unknown_app app_id panelde kayıtlı değil veya yanlış. Panelde Expo Project ID'yi kontrol edin.
Token alınamıyor Fiziksel cihaz kullanın, bildirim iznini kontrol edin, projectId parametresini verdiğinizden emin olun.
Kayıt başarılı ama bildirim gelmiyor Expo projesinde FCM V1 / APNs kimlik bilgileri eksik olabilir. Expo push aracı ile token'a test bildirimi gönderin.
Android'de bildirim sessiz geliyor default ID'li bir bildirim kanalını HIGH önem seviyesiyle oluşturun.