Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test #69

Closed
wants to merge 2 commits into from
Closed

Test #69

wants to merge 2 commits into from

Conversation

ashkan-deriv
Copy link

@ashkan-deriv ashkan-deriv commented Mar 7, 2025

Summary by Sourcery

Implements Progressive Web App (PWA) functionality, including offline support, install prompts, and background synchronization for trades. It also adds an offline indicator and improves the user experience when the application is offline.

New Features:

  • Adds PWA support with offline capabilities, allowing users to continue trading even without an internet connection.
  • Implements background synchronization for trades, ensuring that trades are processed when the user comes back online.
  • Adds an offline indicator to notify users when they are offline.
  • Adds install prompts to allow users to install the application on their devices.
  • Adds push notifications support

- Add web app manifest and service worker
- Create PWA installation prompts and update notifications
- Add offline fallback page with pending trades display
- Implement offline detection and visual indicators
- Create background sync for offline trades
- Add IndexedDB storage for offline persistence
- Update build configuration for PWA assets

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
Copy link

sourcery-ai bot commented Mar 7, 2025

Reviewer's Guide by Sourcery

This pull request introduces Progressive Web App (PWA) support with offline capabilities. It includes a service worker for caching and background synchronization, a manifest file for PWA metadata, and components for prompting installation and update notifications. Additionally, it implements offline trade queuing using Zustand and IndexedDB, ensuring trades are processed when the app regains connectivity.

No diagrams generated as the changes look simple and do not need a visual representation.

File-Level Changes

Change Details Files
Enabled Progressive Web App (PWA) support, providing offline functionality and installability.
  • Added service worker for caching and background sync.
  • Added a manifest file for PWA metadata.
  • Added an offline page to display when the app is offline.
  • Registered the service worker in the application.
  • Added components for prompting the user to install the PWA and notifying them of updates.
rsbuild.config.ts
README.md
src/App.tsx
index.html
src/layouts/MainLayout/MainLayout.tsx
public/service-worker.js
public/offline.html
src/serviceWorkerRegistration.ts
src/components/PWA/PWAPrompt.tsx
src/components/PWA/UpdateNotification.tsx
src/stores/offlineStore.ts
src/components/PWA/PWAProvider.tsx
public/manifest.json
src/components/ui/offline-indicator.tsx
src/components/PWA/index.ts
Implemented offline trade queuing functionality.
  • Added a Zustand store to manage offline state and pending trades.
  • Modified the trade button to queue trades when offline.
  • Added background sync to process pending trades when the app comes back online.
  • Used IndexedDB to persist pending trades while offline.
src/components/TradeButton/TradeButton.tsx
src/stores/offlineStore.ts
public/service-worker.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ashkan-deriv - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding a comment explaining the purpose of the PWAProvider component.
  • The service worker file is quite large; consider using a build tool to minimize it.
Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@@ -41,7 +63,7 @@ export const TradeButton: React.FC<TradeButtonProps> = ({
className
)}
variant="default"
onClick={onClick}
onClick={handleClick}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider replacing alert with a non-blocking notification.

Using alert can disrupt the user experience. If possible, integrate a more seamless and non-blocking mechanism to notify users when offline.

Suggested implementation:

import React from 'react';
import { toast } from 'react-toastify';
    if (!navigator.onLine) {
        toast.error('You are offline, please check your connection.');
        return;
    }

- Offline functionality
- Install prompts for adding to home screen
- Background synchronization for offline trades
- Push notifications support
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Provide details about push notification implementation and usage

The documentation mentions push notification support, but lacks details about how it's implemented and used within the app. Adding this information would be beneficial.

Suggested change
- Push notifications support
- Push notifications support
- Implemented with the Web Push API, allowing real-time notifications.
- A dedicated service worker listens for push events and displays notifications even when the app is in the background.
- The server triggers notifications via a configured push service, ensuring users are alerted to relevant trading updates.

@@ -0,0 +1,223 @@
// Cache name with version for easy updating
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting caching strategies, background sync/IndexedDB interactions, and push notification handling into separate modules to improve maintainability and scalability of the service worker.

You’re combining several distinct responsibilities in one file. This can make it harder to maintain or extend things later. For example, you might extract caching logic, background sync/IndexedDB interactions, and push notification handling into separate modules. Consider the following approach:

  1. Extract caching strategies into a module (e.g., sw-cache.js):

    // sw-cache.js
    export const CACHE_NAME = "champion-trader-v1";
    export const STATIC_ASSETS = ["/", "/index.html", "/offline.html", "/manifest.json", "/favicon.ico", "/logo.png", "/logo192.png", "/logo512.png", "/icons/trade.png"];
    
    export async function cacheFirstStrategy(request) {
        const cachedResponse = await caches.match(request);
        if (cachedResponse) return cachedResponse;
        try {
            const networkResponse = await fetch(request);
            if (networkResponse && networkResponse.status === 200) {
                const cache = await caches.open(CACHE_NAME);
                cache.put(request, networkResponse.clone());
            }
            return networkResponse;
        } catch (error) {
            if (request.headers.get("accept").includes("text/html")) {
                return caches.match("/offline.html");
            }
            throw error;
        }
    }
    
    export async function networkFirstStrategy(request) {
        try {
            const networkResponse = await fetch(request);
            if (networkResponse && networkResponse.status === 200) {
                const cache = await caches.open(CACHE_NAME);
                cache.put(request, networkResponse.clone());
            }
            return networkResponse;
        } catch (error) {
            const cachedResponse = await caches.match(request);
            if (cachedResponse) return cachedResponse;
            throw error;
        }
    }
  2. Extract background sync and IndexedDB logic (e.g., sw-sync.js):

    // sw-sync.js
    async function openDatabase() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open("ChampionTraderDB", 1);
            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                if (!db.objectStoreNames.contains("pendingTrades")) {
                    db.createObjectStore("pendingTrades", { keyPath: "id" });
                }
            };
            request.onsuccess = (event) => {
                const db = event.target.result;
                resolve({
                    getAll: (storeName) => {
                        return new Promise((resolve, reject) => {
                            const transaction = db.transaction(storeName, "readonly");
                            const store = transaction.objectStore(storeName);
                            const request = store.getAll();
                            request.onsuccess = () => resolve(request.result);
                            request.onerror = () => reject(request.error);
                        });
                    },
                    delete: (storeName, id) => {
                        return new Promise((resolve, reject) => {
                            const transaction = db.transaction(storeName, "readwrite");
                            const store = transaction.objectStore(storeName);
                            const request = store.delete(id);
                            request.onsuccess = () => resolve();
                            request.onerror = () => reject(request.error);
                        });
                    },
                });
            };
            request.onerror = () => reject(request.error);
        });
    }
    
    export async function syncPendingTrades() {
        try {
            const db = await openDatabase();
            const pendingTrades = await db.getAll("pendingTrades");
            for (const trade of pendingTrades) {
                try {
                    const response = await fetch("/api/trade", {
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify(trade),
                    });
                    if (response.ok) await db.delete("pendingTrades", trade.id);
                } catch (error) {
                    console.error("Failed to sync trade:", error);
                }
            }
        } catch (error) {
            console.error("Sync failed:", error);
        }
    }
  3. Extract push notification handlers (e.g., sw-push.js):

    // sw-push.js
    export function handlePushEvent(event) {
        if (!event.data) return;
        const data = event.data.json();
        const options = {
            body: data.body,
            icon: "/logo192.png",
            badge: "/logo192.png",
            vibrate: [100, 50, 100],
            data: { url: data.url || "/" },
        };
        event.waitUntil(self.registration.showNotification(data.title, options));
    }
    
    export function handleNotificationClick(event) {
        event.notification.close();
        event.waitUntil(
            clients.matchAll({ type: "window" }).then((clientList) => {
                for (const client of clientList) {
                    if (client.url === event.notification.data.url && "focus" in client) {
                        return client.focus();
                    }
                }
                if (clients.openWindow) {
                    return clients.openWindow(event.notification.data.url);
                }
            })
        );
    }
  4. Refactor the main Service Worker (sw.js) to import and use these modules:

    importScripts('sw-cache.js', 'sw-sync.js', 'sw-push.js');
    
    self.addEventListener("install", (event) => {
        event.waitUntil(
            caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
        );
        self.skipWaiting();
    });
    
    self.addEventListener("activate", (event) => {
        event.waitUntil(
            caches.keys().then((cacheNames) =>
                Promise.all(
                    cacheNames.filter((name) => name !== CACHE_NAME)
                              .map((name) => caches.delete(name))
                )
            )
        );
        self.clients.claim();
    });
    
    self.addEventListener("fetch", (event) => {
        if (!event.request.url.startsWith(self.location.origin)) return;
        if (event.request.url.includes("/api/") ||
            event.request.url.includes("/sse/") ||
            event.request.headers.get("accept").includes("application/json")) {
            event.respondWith(networkFirstStrategy(event.request));
        } else {
            event.respondWith(cacheFirstStrategy(event.request));
        }
    });
    
    self.addEventListener("sync", (event) => {
        if (event.tag === "sync-trades") event.waitUntil(syncPendingTrades());
    });
    
    self.addEventListener("push", handlePushEvent);
    self.addEventListener("notificationclick", handleNotificationClick);

This modular approach keeps different PWA concerns isolated, making each part easier to test, maintain, and extend—without altering functionality.

Copy link

@devloai devloai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've completed a thorough code review of the Pull Request that implements Progressive Web App (PWA) functionality for the Champion Trader application. The implementation adds important features like service worker for offline capabilities, PWA installation prompt, update notifications, offline indicator, background sync for trades, and proper manifest configuration. I've identified 12 issues that should be addressed before merging, ranging from variable naming improvements to more critical issues like missing message handlers and type safety concerns. Addressing these issues will make the PWA implementation more robust and aligned with best practices.

I'm available to help with any questions or follow-up tasks related to this review or other aspects of the repository.

Follow-up suggestions: @devloai help implement the missing SKIP_WAITING message handler in the service worker

import UpdateNotification from "./UpdateNotification";

export const PWAProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more descriptive name for the registration state, such as serviceWorkerRegistration to make it clear what kind of registration this is.

const CACHE_NAME = "champion-trader-v1";

// Assets to cache on install
const STATIC_ASSETS = [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The STATIC_ASSETS array is not complete. You are missing the actual JavaScript and CSS files that would be generated during build. Consider using a build-time plugin to generate this list automatically, or at minimum add a comment to explain that additional assets need to be added after build.


interface OfflineState {
isOnline: boolean;
pendingTrades: any[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any[] for pendingTrades is not type-safe. Consider creating a proper interface for the trade objects that will be stored offline.

});

// Show a notification that the trade will be processed when online
// This would typically be handled by a notification system
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use alert() for user notifications as it's not a good user experience. Instead, integrate with the application's notification system.

// Store trade for later synchronization
addPendingTrade({
type: title,
instrument: label,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trade object structure being stored here does not match what the IndexedDB is expecting. Ensure consistent data structure between components.

});

// Fetch event - serve from cache, fallback to network
self.addEventListener("fetch", (event) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API detection logic might miss some API calls if they don't include /api/ or /sse/ in the URL. Consider a more robust detection method or ensure all API endpoints follow this pattern consistently throughout the application.

});

// Background sync for pending trades
async function syncPendingTrades() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background sync implementation assumes a hardcoded endpoint /api/trade for all trade synchronizations. This should match the actual API endpoint used in the application. Consider making this configurable or consistent with the rest of the app.

// Tell waiting service worker to take control immediately
waitingWorker.postMessage({ type: "SKIP_WAITING" });

// Reload once the new service worker has taken control
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition here. The service worker might take control before the event listener is registered. Consider registering the event listener before sending the message to the service worker.

@@ -0,0 +1,223 @@
// Cache name with version for easy updating
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service worker doesn't listen for the SKIP_WAITING message that UpdateNotification.tsx sends. Add a message event listener to handle this message type. You'll need to add code like: self.addEventListener("message", (event) => { if (event.data && event.data.type === "SKIP_WAITING") { self.skipWaiting(); } });

navigator.serviceWorker.ready.then((registration) => {
// Trigger background sync if available
if ("sync" in registration) {
(registration as any).sync
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type casting (registration as any).sync suggests TypeScript is missing proper typings for the Background Sync API. Consider adding a proper type definition or interface for this API to maintain type safety.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant