Geliştiriciler · API v1.0.0
Flutter Entegrasyonu
Bu rehber Flutter ile geliştirilen Android ve iOS uygulamaları içindir. Genel API referansı için Genel bakış.
Desteklenen sürümler
| Bileşen | Minimum | Önerilen |
|---|---|---|
| Flutter | 3.22 | güncel stable |
| Dart | 3.4 | güncel |
firebase_messaging |
15.x | güncel |
| Android | 6.0 (API 23) | targetSdk 35+ |
| iOS | 13.0 | 15.0+ |
Nasıl çalışır?
NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder. Flutter uygulamanız:
firebase_messagingile native push token'ı alır (Android: FCM token, iOS: APNs token),- Bu token'ı Expo token servisinde bir Expo Push Token'a dönüştürür,
- Expo Push Token'ı NotiPilot'a
register-deviceile kaydeder.
Uygulamanızın Expo ile yazılmış olması gerekmez; Expo yalnızca teslimat altyapısıdır.
1. Ön hazırlık (bir kez)
- Firebase projesini Flutter uygulamanıza bağlayın:
dart pub global activate flutterfire_cli flutterfire configure - expo.dev üzerinde bir proje oluşturun ve proje ID'sini (UUID) not edin.
- Kimlik bilgilerini Expo projesine yükleyin (expo.dev → Proje → Credentials):
- Android: Firebase → Proje Ayarları → Hizmet Hesapları → Yeni özel anahtar ile indirilen JSON → FCM V1 service account key
- iOS: Apple Developer'dan oluşturulan APNs
.p8anahtarı → Push Key (adımlar için iOS rehberi)
- NotiPilot panelinde uygulamanızı ekleyin: Expo Project ID, Expo Access Token, Android Package, iOS Bundle ID.
2. Bağımlılıklar
flutter pub add firebase_core firebase_messaging http flutter_secure_storage uuid package_info_plus
iOS: Xcode'da ios/Runner.xcworkspace → Runner target → Signing & Capabilities → Push Notifications ve Background Modes → Remote notifications ekleyin.
Android: android/app/build.gradle içinde minSdk 23 olmalıdır. Android 13+ bildirim izni firebase_messaging tarafından istenir.
3. lib/notipilot.dart
import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:ui' show PlatformDispatcher;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:uuid/uuid.dart';
class NotiPilot {
NotiPilot._();
static final NotiPilot instance = NotiPilot._();
static const _baseUrl = 'https://app.notipilot.com/api/v1';
static const _appId = 'EXPO-PROJE-ID-NIZ'; // Paneldeki "Expo Project ID"
static const _uidKey = 'notipilot_device_uid';
final _storage = const FlutterSecureStorage();
final _messaging = FirebaseMessaging.instance;
Future<String> deviceUid() async {
var uid = await _storage.read(key: _uidKey);
if (uid == null) {
uid = const Uuid().v4();
await _storage.write(key: _uidKey, value: uid);
}
return uid;
}
/// Uygulama açılışında çağırın. İzin ister, token'ı alır ve NotiPilot'a kaydeder.
Future<Map<String, dynamic>?> registerDevice({Map<String, Object?> extraAttributes = const {}}) async {
final settings = await _messaging.requestPermission(alert: true, badge: true, sound: true);
if (settings.authorizationStatus == AuthorizationStatus.denied) return null;
final uid = await deviceUid();
final expoToken = await _getExpoPushToken(uid);
if (expoToken == null) return null;
final info = await PackageInfo.fromPlatform();
final locale = PlatformDispatcher.instance.locale;
return _post('/register-device', {
'app_id': _appId,
'device_uid': uid,
'token': expoToken,
'platform': Platform.isIOS ? 'ios' : 'android',
'provider': 'expo',
'attributes': {
'locale': locale.languageCode, // "tr"
'country': locale.countryCode, // "TR"
'app_version': info.version,
...extraAttributes, // örn. {'city': 'Istanbul'}
},
});
}
/// Kullanıcı giriş yaptığında çağırın.
Future<Map<String, dynamic>> identify(String externalId, {Map<String, Object?>? attributes}) async {
return _post('/identify-device', {
'app_id': _appId,
'device_uid': await deviceUid(),
'external_id': externalId,
if (attributes != null) 'attributes': attributes,
});
}
/// Token yenilendiğinde NotiPilot'u güncel tutar. Uygulama başlangıcında bir kez çağırın.
void listenForTokenRefresh() {
_messaging.onTokenRefresh.listen((_) => registerDevice());
}
Future<String?> _getExpoPushToken(String uid) async {
final isIOS = Platform.isIOS;
String? deviceToken;
if (isIOS) {
// APNs token uygulama açılışından kısa süre sonra hazır olur
for (var i = 0; i < 5 && deviceToken == null; i++) {
deviceToken = await _messaging.getAPNSToken();
if (deviceToken == null) await Future.delayed(const Duration(seconds: 1));
}
} else {
deviceToken = await _messaging.getToken();
}
if (deviceToken == null) return null;
final info = await PackageInfo.fromPlatform();
final res = await http.post(
Uri.parse('https://exp.host/--/api/v2/push/getExpoPushToken'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'type': isIOS ? 'apns' : 'fcm',
'deviceId': uid.toLowerCase(),
'development': isIOS && kDebugMode, // iOS debug build'leri APNs sandbox kullanır
'appId': info.packageName, // Android package / iOS bundle id
'deviceToken': deviceToken,
'projectId': _appId,
}),
);
if (res.statusCode != 200) {
debugPrint('[NotiPilot] Expo token exchange failed: ${res.statusCode} ${res.body}');
return null;
}
return (jsonDecode(res.body)['data'] as Map<String, dynamic>)['expoPushToken'] as String;
}
Future<Map<String, dynamic>> _post(String path, Map<String, Object?> body, [int attempt = 0]) async {
final res = await http.post(
Uri.parse('$_baseUrl$path'),
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: jsonEncode(body),
);
final json = res.body.isEmpty ? <String, dynamic>{} : jsonDecode(res.body) as Map<String, dynamic>;
if ((res.statusCode == 429 || res.statusCode >= 500) && attempt < 3) {
final wait = (json['retry_after'] as num?)?.toInt() ?? (1 << attempt);
await Future.delayed(Duration(seconds: wait));
return _post(path, body, attempt + 1);
}
if (res.statusCode >= 400) {
debugPrint('[NotiPilot] ${res.statusCode} ${json['error']} ${json['errors'] ?? json['message']}');
}
return json;
}
/// Panelden gönderilen özel `data` alanını döndürür.
static Map<String, dynamic> customData(RemoteMessage message) {
final raw = message.data['body'];
if (raw is String) {
try {
return jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {}
}
return Map<String, dynamic>.from(message.data);
}
}
4. main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'notipilot.dart';
@pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
// iOS: uygulama ön plandayken de bildirimi göster
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true, badge: true, sound: true,
);
NotiPilot.instance.registerDevice(extraAttributes: {'city': 'Istanbul'});
NotiPilot.instance.listenForTokenRefresh();
// Bildirime tıklanınca
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final data = NotiPilot.customData(message);
// örn. data['screen'] == 'product' → Navigator ile yönlendir
});
runApp(const MyApp());
}
Kullanıcı giriş yaptığında: await NotiPilot.instance.identify(user.id);
Android ön plan bildirimleri: Uygulama açıkken Android bildirimi otomatik göstermez. Ön planda da bildirim göstermek istiyorsanız
flutter_local_notificationspaketi ileFirebaseMessaging.onMessageiçinde,defaultID'li bir kanal üzerinden bildirim oluşturun. Başlık içinmessage.notification?.title ?? message.data['title'], metin içinmessage.notification?.body ?? message.data['message']kullanın.
Kontrol listesi
-
flutterfire configureçalıştırıldı - FCM V1 anahtarı ve APNs
.p8anahtarı Expo projesine yüklendi - Paneldeki Expo Project ID =
_appId, paket adı / bundle ID panelde doğru - iOS'ta Push Notifications + Remote notifications capability'leri açık
-
listenForTokenRefresh()başlangıçta çağrılıyor -
countryve mümkünsecityattributes olarak gönderiliyor
Sık karşılaşılan sorunlar
| Belirti | Çözüm |
|---|---|
iOS'ta getAPNSToken() null |
Fiziksel cihaz kullanın, capability'leri ve bildirim iznini kontrol edin. |
Token dönüşümü 4xx dönüyor |
projectId ve appId (paket adı / bundle ID) değerlerini kontrol edin. |
| Kayıt başarılı, bildirim gelmiyor | Expo projesinde ilgili platformun kimlik bilgisi eksik. |
404 unknown_app |
app_id panelde kayıtlı değil. |