aybimyazilim/laravel-expo-notifications 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

aybimyazilim/laravel-expo-notifications

最新稳定版本:v1.0.0

Composer 安装命令:

composer require aybimyazilim/laravel-expo-notifications

包简介

Laravel için Expo Push Notifications servisi - React Native Expo uygulamalarına bildirim gönderme paketi

README 文档

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.

统计信息

  • 总下载量: 30
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-09-12