laravel-expo-notifications maintained by aybimyazilim
Description
Laravel için Expo Push Notifications servisi - React Native Expo uygulamalarına bildirim gönderme paketi
Author
Last update
2025/09/12 10:11
(dev-master)
License
Downloads
54
Tags
Laravel Expo Notifications
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.