How to Serve Dynamic Icons and App Names in Angular PWAs Based on Subdomain Without Breaking...

Discussão em 'Angular' iniciado por Harsh Patel, Dezembro 5, 2024.

  1. Harsh Patel

    Harsh Patel Guest

    I am working on an Angular PWA where I need to serve the manifest.webmanifest file dynamically from the backend. For this, I have set up a proxy in my Apache server configuration to route requests to the backend Laravel API, which generates the manifest dynamically.

    This setup works fine for enabling the PWA service worker and the "Install PWA" functionality. However, I encounter a problem when attempting to update the PWA after deploying a new Angular build. The update process consistently fails with the VERSION_INSTALLATION_FAILED error.

    On the other hand, if I use a static manifest.webmanifest file generated by Angular's build system, the PWA updates successfully without issues.

    Below is the error log from the browser console:

    [​IMG]

    My Setup


    Apache Configuration
    ​

    ProxyPreserveHost On

    # Directly Proxy manifest.webmanifest to Laravel's dynamic-manifest endpoint
    ProxyPass /ordering/manifest.webmanifest http://192.168.1.205:8000/dynamic-manifest
    ProxyPassReverse /ordering/manifest.webmanifest http://192.168.1.205:8000/dynamic-manifest


    Laravel Backend: The dynamic-manifest endpoint generates the manifest dynamically based on the subdomain and includes headers like ETag and Cache-Control
    ​

    Route::get('/dynamic-manifest', function (Request $request) {
    // // Path to the static manifest file in the public directory
    // $manifestPath = public_path('manifest.webmanifest');
    // // Check if the file exists
    // if (!file_exists($manifestPath)) {
    // abort(404, 'Manifest file not found');
    // }

    // // Read the contents of the manifest file
    // $manifestContent = file_get_contents($manifestPath);

    // // Return the manifest content with appropriate headers
    // return response($manifestContent)
    // ->header('Content-Type', 'application/manifest+json')
    // // ->header('Cache-Control', 'public, max-age=31536000')
    // ->header('ETag', sha1($manifestContent));

    $subdomain = explode('.', $request->getHost())[0]; // Get subdomain

    // Map subdomains to app details
    $config = [
    'subdomain1' => [
    'name' => 'Subdomain 1 Store',
    'short_name' => 'Store1',
    'icon' => '/icons/store1-icon.png',
    'theme_color' => '#FF5733',
    ],
    'subdomain2' => [
    'name' => 'Subdomain 2 Store',
    'short_name' => 'Store2',
    'icon' => '/icons/store2-icon.png',
    'theme_color' => '#33FF57',
    ],
    'devtest1' => [
    'name' => 'Hp\'s Pwa Test Edit test',
    'short_name' => 'hp\'s-pwa-test',
    'icon' => '/icons/store2-icon.png',
    'theme_color' => '#33FF57',
    ],
    // Default config
    'default' => [
    'name' => 'Default Store',
    'short_name' => 'Store',
    'icon' => '/icons/store-icon.png',
    'theme_color' => '#000000',
    ],
    ];

    // Get config based on subdomain or fallback to default
    $settings = $config[$subdomain] ?? $config['default'];

    $manifest = [
    'name' => $settings['name'],
    'short_name' => $settings['short_name'],
    'theme_color' => $settings['theme_color'],
    "background_color" => "#fafafa",
    'display' => 'standalone',
    'scope' => './',
    'start_url' => './',
    'icons' => [
    [
    "src" => "manifest-pwa-img?size=72",
    "sizes" => "72x72",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=96",
    "sizes" => "96x96",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=128",
    "sizes" => "128x128",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=144",
    "sizes" => "144x144",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=152",
    "sizes" => "152x152",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=192",
    "sizes" => "192x192",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=384",
    "sizes" => "384x384",
    "type" => "image/png",
    "purpose" => "maskable any"
    ],
    [
    "src" => "manifest-pwa-img?size=512",
    "sizes" => "512x512",
    "type" => "image/png",
    "purpose" => "maskable any"
    ]
    ],
    ];

    // Generate ETag using sha1, similar to Angular's hashing algorithm
    $etag = sha1(json_encode($manifest));

    // Handle conditional requests with If-None-Match
    if ($request->header('If-None-Match') === $etag) {
    return response('', 304); // Respond with 304 if ETag matches
    }

    // Optional: Set Last-Modified based on a known timestamp (e.g., file modification time)
    $lastModified = Carbon::now(); // Replace with a fixed date if needed
    // $lastModified = Carbon::parse('2024-12-05T11:55:31.000Z');

    // Create the response with headers
    $response = response()->json($manifest);
    $response->header('Content-Type', 'application/manifest+json');
    $response->header('Cache-Control', 'no-cache, must-revalidate'); // Ensures revalidation
    $response->header('ETag', $etag);
    $response->header('Last-Modified', $lastModified->toRfc7231String());

    return $response;

    // return response(json_encode($manifest, JSON_PRETTY_PRINT));
    })->middleware(ManifestHeaders::class);


    Angular PWA Update Manager Service: I use Angular's SwUpdate service to manage updates
    ​

    import { DOCUMENT } from '@angular/common';
    import { ApplicationRef, Inject, Injectable } from '@angular/core';
    import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';

    @Injectable({
    providedIn: 'root'
    })
    export class UpdateManagerService {

    constructor(
    private appRef: ApplicationRef,
    private swUpdate: SwUpdate,
    @Inject(DOCUMENT) private document: Document,
    ) {
    console.warn("UpdateManagerService initialized");
    this.initialize();
    }

    /**
    * Initializes the update manager service
    */
    private initialize() {
    if (this.swUpdate.isEnabled) {
    console.log("Service Worker is enabled");
    this.initializeUpdateNotifications();

    // Optionally check for an update immediately if the service worker is registered
    this.isServiceWorkerRegistered().then(isRegistered => {
    if (isRegistered) {
    this.checkForUpdateImmediately();
    } else {
    console.warn("Service Worker is not registered. Skipping immediate update check.");
    }
    }).catch(err => {
    console.error('Failed to check Service Worker registration:', err);
    });
    } else {
    console.warn("Service Worker is not enabled.");
    }
    }

    /**
    * Initializes notifications for update events
    */
    private initializeUpdateNotifications() {
    this.swUpdate.versionUpdates.subscribe(evt => {
    switch (evt.type) {
    case 'VERSION_DETECTED':
    console.log(`New version detected: ${evt.version.hash}`);
    break;
    case 'VERSION_READY':
    // this.promptUserToUpdate(evt);
    this.updateApplication();
    break;
    case 'VERSION_INSTALLATION_FAILED':
    console.error(`Failed to install version '${evt.version.hash}': ${evt.error}`);
    let alertMessage = "We encountered a problem while updating the application. Please clear your browser cache manually and reload the page to resolve the issue.";
    alert(alertMessage);
    break;
    }
    });

    // For more details about unrecoverable states, read the Angular documentation here:
    // https://angular.dev/ecosystem/service-workers/communications#handling-an-unrecoverable-state
    this.swUpdate.unrecoverable.subscribe(event => {
    console.error(`An error occurred that we cannot recover from:\n${event.reason}\n\nPlease reload the page.`);
    console.warn('It looks like there’s an issue that needs your attention. To ensure the best experience, please refresh the page.');
    alert("Update Needed! We encountered a situation that requires your attention. Please refresh the page to continue.");
    });
    }

    /**
    * Checks if the Service Worker is registered
    */
    private async isServiceWorkerRegistered(): Promise<boolean> {
    if ('serviceWorker' in navigator) {
    const registrations = await navigator.serviceWorker.getRegistrations();
    return registrations.length > 0;
    }
    return false;
    }

    private checkForUpdateImmediately() {
    if (this.swUpdate.isEnabled) {
    this.swUpdate.checkForUpdate().then(() => {
    console.log('Checked for updates on initialization.');
    }).catch(err => {
    console.error('Failed to check for updates immediately:', err);
    });
    }
    }

    /**
    * Activates the update and reloads the page
    */
    private updateApplication() {
    this.swUpdate.activateUpdate().then(() => {
    console.log('Application updated to the latest version.');
    alert("The application has been updated. The page will now reload.");
    this.document.location.reload();
    }).catch(err => {
    console.error('Failed to activate update:', err);
    alert("Failed to activate the update. Please try refreshing the page or clear the cache if the issue persists.");
    });
    }
    }

    Observations

    • Static manifest.webmanifest: PWA updates work properly.
    • Dynamic manifest.webmanifest: Always results in VERSION_INSTALLATION_FAILED.
    What I've Tried

    • Ensuring ETag and Cache-Control headers are correctly set for the dynamic manifest.
    • Adding Last-Modified headers to the dynamic response.
    • Verifying the dynamic manifest is accessible and valid through browser DevTools.
    Question


    Why does serving a dynamic manifest.webmanifest cause the PWA update to fail with VERSION_INSTALLATION_FAILED? How can I ensure dynamic manifests work seamlessly with Angular's service worker update mechanism?

    Continue reading...

Compartilhe esta Página