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

v1.0.0 2025-09-12 08:11 UTC

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 token
  • InvalidMessageException: 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

  1. Fork edin
  2. Feature branch oluşturun (git checkout -b feature/amazing-feature)
  3. Değişikliklerinizi commit edin (git commit -m 'Add amazing feature')
  4. Branch'i push edin (git push origin feature/amazing-feature)
  5. 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.