import httpx import logging from typing import Optional, Dict, Any from core.config import settings logger = logging.getLogger(__name__) async def send_push_notification( push_token: str, title: str, body: str, data: Optional[Dict[str, Any]] = None ) -> bool: """ Sends a push notification to a specific Expo push token. Args: push_token: The recipient's Expo push token. title: The title of the notification. body: The main message content of the notification. data: Optional dictionary containing extra data to send with the notification. Returns: True if the notification was sent successfully (according to Expo API), False otherwise. """ if not push_token: logger.warning("Attempted to send notification but no push token provided.") return False message = { "to": push_token, "sound": "default", "title": title, "body": body, "priority": "high", "channelId": "default", } if data: message["data"] = data async with httpx.AsyncClient() as client: try: response = await client.post( settings.EXPO_PUSH_API_URL, headers={ "Accept": "application/json", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/json", }, json=message, timeout=10.0, ) response.raise_for_status() # Raise exception for 4xx/5xx responses response_data = response.json() logger.debug(f"Expo push API response: {response_data}") # Check for top-level errors first if "errors" in response_data: error_messages = [ err.get("message", "Unknown error") for err in response_data["errors"] ] logger.error( f"Expo API returned errors for {push_token[:10]}...: {'; '.join(error_messages)}" ) return False # Check the status in the data field receipt = response_data.get("data") # if receipts is a list if receipt: status = receipt.get("status") if status == "ok": logger.info( f"Successfully sent push notification to token: {push_token[:10]}..." ) return True else: # Log details if the status is not 'ok' error_details = receipt.get("details") error_message = receipt.get("message") logger.error( f"Failed to send push notification to {push_token[:10]}... " f"Expo status: {status}, Message: {error_message}, Details: {error_details}" ) return False else: # Log if 'data' is missing, not a list, or an empty list logger.error( f"Unexpected Expo API response format or empty 'data' field for {push_token[:10]}... " f"Response: {response_data}" ) return False except httpx.HTTPStatusError as e: logger.error( f"HTTP error sending push notification to {push_token[:10]}...: {e.response.status_code} - {e.response.text}" ) return False except httpx.RequestError as e: logger.error( f"Network error sending push notification to {push_token[:10]}...: {e}" ) return False except Exception as e: logger.exception( f"Unexpected error sending push notification to {push_token[:10]}...: {e}" ) return False