PWA Overview

WI-CARPOOL is built as a Progressive Web App, providing native app-like experiences with web technologies.

📱 App Installation

Users can install the app on their home screen for quick access.

🔄 Offline Support

Core functionality works even without internet connection.

🔔 Push Notifications

Real-time notifications for ride updates and messages.

⚡ Fast Loading

Service worker caching for instant loading times.

Service Worker Implementation

Service Worker Registration

// src/utils/pwa.ts
export function registerSW() {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
      try {
        const registration = await navigator.serviceWorker.register('/sw.js')
        console.log('SW registered: ', registration)
        
        // Handle updates
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing
          if (newWorker) {
            newWorker.addEventListener('statechange', () => {
              if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                // New content available
                showUpdateAvailableNotification()
              }
            })
          }
        })
      } catch (error) {
        console.log('SW registration failed: ', error)
      }
    })
  }
}

function showUpdateAvailableNotification() {
  // Show user notification about available update
  if (confirm('New version available! Reload to update?')) {
    window.location.reload()
  }
}

Caching Strategy

// public/sw.js
const CACHE_NAME = 'wicarpool-v1.0.0'
const STATIC_CACHE = 'static-v1'
const DYNAMIC_CACHE = 'dynamic-v1'

const STATIC_ASSETS = [
  '/',
  '/manifest.json',
  '/icon-192.png',
  '/icon-512.png',
  // Add other static assets
]

// Install event - cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => {
      return cache.addAll(STATIC_ASSETS)
    })
  )
})

// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    // Network first for API calls
    event.respondWith(networkFirstStrategy(event.request))
  } else {
    // Cache first for static assets
    event.respondWith(cacheFirstStrategy(event.request))
  }
})

async function cacheFirstStrategy(request) {
  const cache = await caches.open(DYNAMIC_CACHE)
  const cachedResponse = await cache.match(request)
  
  if (cachedResponse) {
    return cachedResponse
  }
  
  try {
    const response = await fetch(request)
    cache.put(request, response.clone())
    return response
  } catch (error) {
    // Return offline fallback
    return caches.match('/offline.html')
  }
}

async function networkFirstStrategy(request) {
  try {
    const response = await fetch(request)
    const cache = await caches.open(DYNAMIC_CACHE)
    cache.put(request, response.clone())
    return response
  } catch (error) {
    const cache = await caches.open(DYNAMIC_CACHE)
    return cache.match(request)
  }
}

App Installation

Install Prompt Component

// src/components/PWAInstallButton.tsx
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Download } from 'lucide-react'

export function PWAInstallButton() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
  const [showInstall, setShowInstall] = useState(false)

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault()
      setDeferredPrompt(e)
      setShowInstall(true)
    }

    window.addEventListener('beforeinstallprompt', handler)

    // Check if already installed
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setShowInstall(false)
    }

    return () => window.removeEventListener('beforeinstallprompt', handler)
  }, [])

  const handleInstall = async () => {
    if (!deferredPrompt) return

    deferredPrompt.prompt()
    const { outcome } = await deferredPrompt.userChoice
    
    if (outcome === 'accepted') {
      setShowInstall(false)
    }
    
    setDeferredPrompt(null)
  }

  if (!showInstall) return null

  return (
    <Button onClick={handleInstall} variant="outline" size="sm">
      <Download className="h-4 w-4 mr-2" />
      Install App
    </Button>
  )
}

Web App Manifest

// public/manifest.json
{
  "name": "WI-CARPOOL - Ride Sharing App",
  "short_name": "WI-CARPOOL",
  "description": "Modern ride-sharing application connecting passengers and drivers",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3b82f6",
  "orientation": "portrait-primary",
  "scope": "/",
  "lang": "en",
  "icons": [
    {
      "src": "/icon-72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "categories": ["travel", "transportation"],
  "screenshots": [
    {
      "src": "/screenshot-desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/screenshot-mobile.png",
      "sizes": "375x812",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ]
}

Push Notifications

Notification Permission

// src/utils/notifications.ts
export async function requestNotificationPermission(): Promise<boolean> {
  if (!('Notification' in window)) {
    console.log('This browser does not support notifications')
    return false
  }

  if (Notification.permission === 'granted') {
    return true
  }

  if (Notification.permission === 'denied') {
    return false
  }

  const permission = await Notification.requestPermission()
  return permission === 'granted'
}

export async function subscribeUserToPush(): Promise<PushSubscription | null> {
  const registration = await navigator.serviceWorker.ready
  
  try {
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: process.env.VITE_VAPID_PUBLIC_KEY
    })
    
    // Send subscription to server
    await fetch('/api/notifications/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscription)
    })
    
    return subscription
  } catch (error) {
    console.error('Failed to subscribe to push notifications:', error)
    return null
  }
}

export function showLocalNotification(title: string, options: NotificationOptions = {}) {
  if (Notification.permission === 'granted') {
    new Notification(title, {
      icon: '/icon-192.png',
      badge: '/icon-72.png',
      ...options
    })
  }
}

Push Event Handler

// public/sw.js - Push notification handling
self.addEventListener('push', (event) => {
  if (!event.data) return

  const data = event.data.json()
  const options = {
    body: data.body,
    icon: '/icon-192.png',
    badge: '/icon-72.png',
    tag: data.tag || 'default',
    data: data.data || {},
    actions: data.actions || [],
    requireInteraction: data.requireInteraction || false
  }

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  )
})

self.addEventListener('notificationclick', (event) => {
  event.notification.close()

  const action = event.action
  const data = event.notification.data

  if (action === 'view_ride') {
    event.waitUntil(
      clients.openWindow(`/rides/${data.rideId}`)
    )
  } else if (action === 'open_chat') {
    event.waitUntil(
      clients.openWindow(`/chat/${data.chatId}`)
    )
  } else {
    // Default action - open app
    event.waitUntil(
      clients.openWindow('/')
    )
  }
})

Offline Support

Offline Detection

// src/hooks/useOnlineStatus.ts
import { useState, useEffect } from 'react'

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)

    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)

    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return isOnline
}

// Usage in components
function NetworkStatus() {
  const isOnline = useOnlineStatus()

  if (!isOnline) {
    return (
      <div className="bg-yellow-100 border-l-4 border-yellow-500 p-4">
        <p className="text-yellow-700">
          You're currently offline. Some features may be limited.
        </p>
      </div>
    )
  }

  return null
}

Offline Data Sync

// src/utils/offlineSync.ts
interface OfflineAction {
  id: string
  type: string
  data: any
  timestamp: number
}

class OfflineSync {
  private queue: OfflineAction[] = []
  private isProcessing = false

  constructor() {
    this.loadQueue()
    window.addEventListener('online', () => this.processQueue())
  }

  addAction(type: string, data: any) {
    const action: OfflineAction = {
      id: crypto.randomUUID(),
      type,
      data,
      timestamp: Date.now()
    }

    this.queue.push(action)
    this.saveQueue()

    if (navigator.onLine) {
      this.processQueue()
    }
  }

  private async processQueue() {
    if (this.isProcessing || this.queue.length === 0) return

    this.isProcessing = true

    while (this.queue.length > 0) {
      const action = this.queue[0]

      try {
        await this.executeAction(action)
        this.queue.shift()
        this.saveQueue()
      } catch (error) {
        console.error('Failed to sync action:', action, error)
        break
      }
    }

    this.isProcessing = false
  }

  private async executeAction(action: OfflineAction) {
    switch (action.type) {
      case 'book_ride':
        return fetch('/api/rides/book', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(action.data)
        })
      // Add other action types
      default:
        throw new Error(`Unknown action type: ${action.type}`)
    }
  }

  private saveQueue() {
    localStorage.setItem('offline-queue', JSON.stringify(this.queue))
  }

  private loadQueue() {
    const stored = localStorage.getItem('offline-queue')
    this.queue = stored ? JSON.parse(stored) : []
  }
}

export const offlineSync = new OfflineSync()

Performance Optimizations

Resource Loading

  • Critical resources are cached during service worker installation
  • Non-critical resources are cached on first access
  • Images are optimized and served in modern formats
  • JavaScript bundles are split for efficient loading

Caching Strategies

  • Cache First: Static assets (CSS, JS, images)
  • Network First: API calls and dynamic content
  • Stale While Revalidate: Frequently updated content
  • Cache Only: Offline fallback pages

Best Practices

  • Implement proper cache versioning
  • Provide meaningful offline experiences
  • Handle background sync for critical actions
  • Test thoroughly on various devices and network conditions