aybimyazilim / laravel-expo-notifications
Laravel için Expo Push Notifications servisi - React Native Expo uygulamalarına bildirim gönderme paketi
Installs: 29
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/aybimyazilim/laravel-expo-notifications
Requires
- php: ^8.0
- guzzlehttp/guzzle: ^7.0
- illuminate/http: ^8.0|^9.0|^10.0|^11.0
- illuminate/notifications: ^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- mockery/mockery: ^1.4
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0
This package is not auto-updated.
Last update: 2025-12-20 08:13:51 UTC
README
Bu paket, Laravel uygulamalarından Expo (React Native) uygulamalarına push notification gönderebilmenizi sağlar. Kapsamlı hata yakalama, detaylı loglama ve notification takibi içerir.
✨ Özellikler
- ✅ Expo Push Notifications API entegrasyonu
- ✅ Kapsamlı hata yakalama ve handling
- ✅ Detaylı notification logları
- ✅ Veritabanı tabanlı notification takibi
- ✅ Bulk notification gönderimi
- ✅ İstatistik ve raporlama
- ✅ Queue desteği ve retry mekanizması
- ✅ Token validasyonu
- ✅ Receipt status kontrolü
- ✅ Artisan komutları
🚀 Kurulum
composer require aybimyazilim/laravel-expo-notifications
Config Dosyasını Yayınlayın
php artisan vendor:publish --tag=expo-notifications-config
Migration'ları Yayınlayın ve Çalıştırın
php artisan vendor:publish --tag=expo-notifications-migrations php artisan migrate
⚙️ Konfigürasyon
.env dosyanıza aşağıdaki ayarları ekleyin:
# Expo Notification Ayarları EXPO_NOTIFICATION_TIMEOUT=30 EXPO_DEFAULT_SOUND=default EXPO_DEFAULT_PRIORITY=default EXPO_DEFAULT_CHANNEL_ID=default # Loglama Ayarları EXPO_ENABLE_LOGGING=true EXPO_LOG_REQUESTS=true EXPO_LOG_RESPONSES=true # Queue Ayarları EXPO_QUEUE_CONNECTION=default EXPO_QUEUE_NAME=notifications # Retry Ayarları EXPO_RETRY_ENABLED=true EXPO_RETRY_ATTEMPTS=3 EXPO_RETRY_DELAY=60 # Validasyon EXPO_VALIDATE_TOKENS=true EXPO_MAX_TITLE_LENGTH=100 EXPO_MAX_BODY_LENGTH=200 # Bulk Notification EXPO_BULK_LIMIT=100 EXPO_BATCH_SIZE=20
📱 Kullanım
Basit Notification Sınıfı
<?php namespace App\Notifications; use AybimYazilim\LaravelExpoNotifications\Notifications\ExpoNotification; class NewMessageNotification extends ExpoNotification { protected $message; public function __construct($message) { parent::__construct(); $this->message = $message; } public function toExpo($notifiable): array { return [ 'title' => 'Yeni Mesajınız Var! 💬', 'body' => "Gönderen: {$this->message->sender->name}", 'data' => [ 'type' => 'message', 'message_id' => $this->message->id, 'sender_id' => $this->message->sender_id, 'action' => 'open_chat', ], 'sound' => 'default', 'priority' => 'high', 'channelId' => 'messages', ]; } }
User Model Ayarları
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use AybimYazilim\LaravelExpoNotifications\Models\ExpoNotificationLog; class User extends Authenticatable { // Expo token için routing public function routeNotificationForExpo() { return $this->expo_push_token; } // Notification geçmişi public function expoNotifications() { return $this->morphMany(ExpoNotificationLog::class, 'notifiable'); } // Son notification'ları al public function recentNotifications($limit = 10) { return $this->expoNotifications() ->latest() ->limit($limit) ->get(); } }
Notification Gönderimi
use App\Notifications\NewMessageNotification; use Illuminate\Support\Facades\Notification; // Kullanıcıya gönder $user = User::find(1); $user->notify(new NewMessageNotification($message)); // Birden fazla kullanıcıya gönder $users = User::whereNotNull('expo_push_token')->get(); Notification::send($users, new NewMessageNotification($message)); // Belirli bir token'a gönder Notification::route('expo', 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]') ->notify(new NewMessageNotification($message)); // Bulk gönderim $tokens = ['token1', 'token2', 'token3']; foreach ($tokens as $token) { Notification::route('expo', $token) ->notify(new NewMessageNotification($message)); }
Hata Yakalama
use AybimYazilim\LaravelExpoNotifications\Exceptions\ExpoNotificationException; use AybimYazilim\LaravelExpoNotifications\Exceptions\InvalidTokenException; try { $user->notify(new NewMessageNotification($message)); } catch (InvalidTokenException $e) { Log::error('Geçersiz Expo token: ' . $e->getMessage()); // Token'ı temizle veya güncelle $user->update(['expo_push_token' => null]); } catch (ExpoNotificationException $e) { Log::error('Expo notification hatası: ' . $e->getMessage()); }
📊 İstatistikler ve Loglama
Artisan Komutları
# Notification istatistiklerini görüntüle php artisan expo:stats 24h php artisan expo:stats 7d php artisan expo:stats 30d # Test notification gönder php artisan expo:test-notification "ExponentPushToken[xxx]" "Test Title" "Test Body"
Programatik İstatistikler
use AybimYazilim\LaravelExpoNotifications\Services\ExpoLogService; $logService = new ExpoLogService(); // Son 24 saatin istatistikleri $stats = $logService->getStats('24h'); /* [ 'total' => 250, 'sent' => 240, 'failed' => 10, 'pending' => 0, 'success_rate' => 96.0, 'period' => '24h' ] */ // Başarısız notification'ları al $failedNotifications = $logService->getFailedNotifications(50); // En çok kullanılan notification türleri $topTypes = $logService->getTopNotificationTypes(10, '30d'); // Günlük istatistikler (son 30 gün) $dailyStats = $logService->getDailyStats(30); // Hata analizi $errorAnalysis = $logService->getErrorAnalysis('7d');
🎯 Expo React Native Entegrasyonu
Expo uygulamanızda push token almak için:
import * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; import Constants from 'expo-constants'; import { useEffect, useRef, useState } from 'react'; import { Platform } from 'react-native'; // Notification handler'ı ayarla Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false, }), }); export default function App() { const [expoPushToken, setExpoPushToken] = useState(''); const notificationListener = useRef(); const responseListener = useRef(); useEffect(() => { registerForPushNotificationsAsync().then(token => setExpoPushToken(token)); // Notification alındığında çalışır notificationListener.current = Notifications.addNotificationReceivedListener(notification => { console.log('Notification alındı:', notification); handleNotificationReceived(notification); }); // Notification'a tıklandığında çalışır responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { console.log('Notification\'a tıklandı:', response); handleNotificationResponse(response); }); return () => { Notifications.removeNotificationSubscription(notificationListener.current); Notifications.removeNotificationSubscription(responseListener.current); }; }, []); return ( // Your app content ); } async function registerForPushNotificationsAsync() { let token; if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', }); // Özel kanallar oluştur await Notifications.setNotificationChannelAsync('messages', { name: 'Messages', importance: Notifications.AndroidImportance.HIGH, vibrationPattern: [0, 250, 250, 250], sound: 'message_sound.wav', }); await Notifications.setNotificationChannelAsync('orders', { name: 'Orders', importance: Notifications.AndroidImportance.HIGH, vibrationPattern: [0, 500, 250, 500], sound: 'order_sound.wav', }); await Notifications.setNotificationChannelAsync('promotions', { name: 'Promotions', importance: Notifications.AndroidImportance.DEFAULT, vibrationPattern: [0, 250], sound: 'promotion_sound.wav', }); } if (Device.isDevice) { const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { console.log('Push notification izni alınamadı!'); return; } try { token = (await Notifications.getExpoPushTokenAsync({ projectId: Constants.expoConfig?.extra?.eas?.projectId, })).data; console.log('Expo Push Token:', token); // Token'ı Laravel backend'e gönder await sendTokenToBackend(token); } catch (error) { console.error('Token alma hatası:', error); } } else { console.log('Push notifications sadece fiziksel cihazlarda çalışır'); } return token; } async function sendTokenToBackend(token) { try { const response = await fetch('https://your-laravel-app.com/api/user/expo-token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, body: JSON.stringify({ expo_push_token: token, device_info: { platform: Platform.OS, version: Platform.Version, brand: Device.brand, model: Device.modelName, } }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('Token başarıyla gönderildi:', result); } catch (error) { console.error('Token gönderme hatası:', error); } } // Notification alındığında çağrılır function handleNotificationReceived(notification) { const { title, body, data } = notification.request.content; // Custom handling based on notification type switch (data?.type) { case 'message': // Update message count, show in-app notification etc. updateMessageCount(); break; case 'order_status': // Update order status, refresh order screen refreshOrderStatus(data.order_id); break; case 'promotion': // Show promotion banner, update promotions list showPromotionBanner(data.promotion_id); break; default: console.log('Unknown notification type:', data?.type); } } // Notification'a tıklandığında çağrılır function handleNotificationResponse(response) { const { data } = response.notification.request.content; // Navigate based on notification action switch (data?.action) { case 'open_chat': navigation.navigate('Chat', { messageId: data.message_id, senderId: data.sender_id }); break; case 'open_order': navigation.navigate('OrderDetail', { orderId: data.order_id }); break; case 'open_promotion': navigation.navigate('Promotion', { promotionId: data.promotion_id }); break; case 'open_profile': navigation.navigate('Profile'); break; default: navigation.navigate('Home'); } } // Helper functions function updateMessageCount() { // Update message count in your state management } function refreshOrderStatus(orderId) { // Refresh order status } function showPromotionBanner(promotionId) { // Show promotion banner }
🔧 Laravel API Endpoint'leri
// routes/api.php Route::middleware('auth:sanctum')->group(function () { Route::post('/user/expo-token', [UserController::class, 'updateExpoToken']); Route::get('/notifications/history', [NotificationController::class, 'history']); Route::post('/notifications/mark-as-read', [NotificationController::class, 'markAsRead']); }); // app/Http/Controllers/UserController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; class UserController extends Controller { public function updateExpoToken(Request $request): JsonResponse { $request->validate([ 'expo_push_token' => 'required|string', 'device_info' => 'nullable|array', ]); $user = $request->user(); $user->update([ 'expo_push_token' => $request->expo_push_token, 'device_info' => $request->device_info, 'token_updated_at' => now(), ]); return response()->json([ 'success' => true, 'message' => 'Expo token başarıyla güncellendi', ]); } } // app/Http/Controllers/NotificationController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use AybimYazilim\LaravelExpoNotifications\Services\ExpoLogService; class NotificationController extends Controller { protected $expoLogService; public function __construct(ExpoLogService $expoLogService) { $this->expoLogService = $expoLogService; } public function history(Request $request): JsonResponse { $user = $request->user(); $history = $this->expoLogService->getUserNotificationHistory( get_class($user), $user->id, $request->get('limit', 50) ); return response()->json([ 'success' => true, 'data' => $history, ]); } public function markAsRead(Request $request): JsonResponse { $request->validate([ 'notification_ids' => 'required|array', 'notification_ids.*' => 'integer|exists:expo_notification_logs,id', ]); $user = $request->user(); ExpoNotificationLog::whereIn('id', $request->notification_ids) ->where('notifiable_type', get_class($user)) ->where('notifiable_id', $user->id) ->update(['read_at' => now()]); return response()->json([ 'success' => true, 'message' => 'Notification\'lar okundu olarak işaretlendi', ]); } }
📱 Advanced React Native Features
Notification Kategorileri ve İşlemler
// Notification kategorileri ve işlemleri tanımla import * as Notifications from 'expo-notifications'; // Kategorileri ayarla await Notifications.setNotificationCategoryAsync('message', [ { identifier: 'reply', buttonTitle: 'Yanıtla', textInput: { submitButtonTitle: 'Gönder', placeholder: 'Yanıtınızı yazın...', }, }, { identifier: 'mark_read', buttonTitle: 'Okundu', options: { opensAppToForeground: false, }, }, ]); await Notifications.setNotificationCategoryAsync('order', [ { identifier: 'view_order', buttonTitle: 'Siparişi Görüntüle', }, { identifier: 'track_order', buttonTitle: 'Takip Et', }, ]); // Action response handler responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { const { actionIdentifier, userText } = response; const { data } = response.notification.request.content; switch (actionIdentifier) { case 'reply': handleReplyAction(data.message_id, userText); break; case 'mark_read': handleMarkAsReadAction(data.message_id); break; case 'view_order': navigation.navigate('OrderDetail', { orderId: data.order_id }); break; case 'track_order': navigation.navigate('OrderTracking', { orderId: data.order_id }); break; default: handleNotificationResponse(response); } }); async function handleReplyAction(messageId, replyText) { try { await fetch('https://your-laravel-app.com/api/messages/reply', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, }, body: JSON.stringify({ message_id: messageId, reply: replyText, }), }); } catch (error) { console.error('Reply gönderme hatası:', error); } }
Badge Yönetimi
import * as Notifications from 'expo-notifications'; // Badge sayısını ayarla async function setBadgeCount(count) { await Notifications.setBadgeCountAsync(count); } // Badge sayısını temizle async function clearBadge() { await Notifications.setBadgeCountAsync(0); } // Uygulama açıldığında badge'i temizle useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState) => { if (nextAppState === 'active') { clearBadge(); } }); return () => subscription?.remove(); }, []);
### Bulk Notification Gönderimi
```php
use AybimYazilim\LaravelExpoNotifications\Services\ExpoService;
$expoService = app(ExpoService::class);
$messages = [
[
'to' => 'ExponentPushToken[token1]',
'title' => 'Title 1',
'body' => 'Body 1',
],
[
'to' => 'ExponentPushToken[token2]',
'title' => 'Title 2',
'body' => 'Body 2',
],
// ... daha fazla mesaj
];
$response = $expoService->sendBulkNotifications($messages);
Custom Exception Handling
namespace App\Exceptions; use AybimYazilim\LaravelExpoNotifications\Exceptions\ExpoNotificationException; class Handler extends ExceptionHandler { public function register() { $this->reportable(function (ExpoNotificationException $e) { // Özel Expo notification hata loglama Log::channel('expo')->error('Expo Notification Error', [ 'message' => $e->getMessage(), 'context' => $e->getContext(), 'trace' => $e->getTraceAsString() ]); // Slack, email vb. bildirim gönder if (app()->environment('production')) { $this->notifyAdmins($e); } }); } }
🧪 Testing
use AybimYazilim\LaravelExpoNotifications\Tests\TestCase; use Illuminate\Support\Facades\Http; class ExpoNotificationTest extends TestCase { /** @test */ public function it_sends_expo_notification_successfully() { Http::fake([ 'exp.host/--/api/v2/push/send' => Http::response([ 'data' => [ [ 'status' => 'ok', 'id' => 'test-ticket-id' ] ] ]) ]); $user = User::factory()->create([ 'expo_push_token' => 'ExponentPushToken[test-token]' ]); $user->notify(new NewMessageNotification($message)); Http::assertSent(function ($request) { return str_contains($request->url(), 'exp.host') && $request['title'] === 'Yeni Mesajınız Var! 💬'; }); $this->assertDatabaseHas('expo_notification_logs', [ 'expo_token' => 'ExponentPushToken[test-token]', 'status' => 'sent' ]); } /** @test */ public function it_handles_invalid_token_error() { Http::fake([ 'exp.host/--/api/v2/push/send' => Http::response([ 'data' => [ [ 'status' => 'error', 'message' => 'DeviceNotRegistered', 'details' => [ 'error' => 'DeviceNotRegistered' ] ] ] ]) ]); $this->expectException(InvalidTokenException::class); $user = User::factory()->create([ 'expo_push_token' => 'invalid-token' ]); $user->notify(new NewMessageNotification($message)); } }
📋 Exception Türleri
ExpoNotificationException: Genel Expo notification hatalarıInvalidTokenException: Geçersiz veya kayıtlı olmayan tokenInvalidMessageException: Geçersiz mesaj formatı
🔍 Debugging
Debug modu için .env dosyanıza:
LOG_CHANNEL=stack EXPO_LOG_REQUESTS=true EXPO_LOG_RESPONSES=true
Log dosyalarında detaylı Expo notification gönderim bilgilerini görebilirsiniz.
📝 Changelog
v1.0.0
- ✅ Expo Push Notifications API entegrasyonu
- ✅ Kapsamlı hata yakalama sistemi
- ✅ Detaylı notification loglama
- ✅ Bulk notification desteği
- ✅ İstatistik ve raporlama
- ✅ Receipt status kontrolü
- ✅ Queue desteği ve retry mekanizması
🤝 Katkıda Bulunma
- Fork edin
- Feature branch oluşturun (
git checkout -b feature/amazing-feature) - Değişikliklerinizi commit edin (
git commit -m 'Add amazing feature') - Branch'i push edin (
git push origin feature/amazing-feature) - Pull Request oluşturun
📄 Lisans
Bu paket MIT lisansı altında yayınlanmıştır.
📞 Destek
Herhangi bir sorun yaşarsanız, GitHub Issues üzerinden bildirebilirsiniz.