0

I utilized the guidelines presented in this article to implement Progressive Web App (PWA) features within my Angular application. The primary objective was to establish the capability to detect the availability of new application versions. This would prompt the client-side interface to display a confirmation modal to users, notifying them of the updated application version. The modal message would read as follows: "New version available. Load New Version?" Subsequently, users can proceed to reload the application, ensuring the seamless integration of the latest version.

Code is as below:

ngsw-config.json:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ]
}

app.module.ts:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { BatchAutocompleteLibModule } from '@**service/batch-autocomplete-lib';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import {ToastrModule} from 'ngx-toastr';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LaravelRequestInterceptor } from './common/laravel-request.interceptor';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonService } from './common/common.service';
import { UserService } from './services/auth/user.service';
import { AuthSetupComponent } from './common/components/auth-setup/auth-setup.component';
import { LogoutComponent } from './common/components/logout/logout.component';
import { GoogleTagManagerModule } from 'angular-google-tag-manager';
import {environment} from '@environments/environment';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {AlertModule} from '@app/common/components/alert';
import { StoreModule } from '@ngrx/store';
import { navBarReducer } from './store/navigation/navBar.reducer';

import {
  NgxAwesomePopupModule,
  DialogConfigModule,
  ConfirmBoxConfigModule,
  ToastNotificationConfigModule
} from '@costlydeveloper/ngx-awesome-popup';
import { NotFoundComponent } from './common/components/not-found/not-found.component';
import { ServiceWorkerModule } from '@angular/service-worker';

@NgModule({
  declarations: [
    AppComponent,
    AuthSetupComponent,
    LogoutComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    NgbModule,
    BrowserModule,
    HttpClientModule,
    BatchAutocompleteLibModule.forRoot(environment.autocompleteLibUrl, environment.autocompleteLibToken),
    GoogleTagManagerModule.forRoot({
      id: environment.GTM_id,
      ...environment.GTM_params
    }),
    ToastrModule.forRoot(),
    AlertModule,
    NgxAwesomePopupModule.forRoot({
      colorList: {
        success: '#67E583',
        info: '#3683BC',
        warning: '#E7DD00',
        danger: '#F7775B',
        primary: '#3683BC',
        secondary: '#e46464'
      }
    }), // Essential, mandatory main module.
    DialogConfigModule.forRoot(), // Needed for instantiating dynamic components.
    ConfirmBoxConfigModule.forRoot(), // Needed for instantiating confirm boxes.
    ToastNotificationConfigModule.forRoot(), // Needed for instantiating toast notifications.
    StoreModule.forRoot({ navBar: navBarReducer }), ServiceWorkerModule.register('ngsw-worker.js', {
  enabled: true
  // Register the ServiceWorker as soon as the application is stable
  // or after 30 seconds (whichever comes first).
  // registrationStrategy: 'registerWhenStable:30000'
}),
  ],
  providers: [
    CommonService,
    UserService,
    {provide: HTTP_INTERCEPTORS, useClass: LaravelRequestInterceptor, multi: true},
  ],
  schemas: [
    CUSTOM_ELEMENTS_SCHEMA
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {
    "analytics": "53ed1256-56ee-4746-9a54-3e6bd73c6b2d"
  },
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "***-angular": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/***-angular",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              "src/.htaccess",
              "src/manifest.webmanifest"
            ],
            "styles": [
              "src/styles.scss",
              "node_modules/datatables.net-dt/css/jquery.dataTables.min.css",
              "node_modules/ngx-toastr/toastr.css",
              "node_modules/ng2-daterangepicker/assets/daterangepicker.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/popper.js/dist/umd/popper.js",
              "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
              "node_modules/apexcharts/dist/apexcharts.min.js",
              "node_modules/datatables.net/js/jquery.dataTables.min.js"
            ],
            "vendorChunk": true,
            "extractLicenses": false,
            "buildOptimizer": false,
            "sourceMap": true,
            "optimization": false,
            "namedChunks": true,
            "serviceWorker": true,
            "ngswConfigPath": "ngsw-config.json"
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "10mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "20kb"
                }
              ]
            },
            "staging": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.staging.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            },
            "development": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.development.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            },
            "tfdev": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.tfdev.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            },
            "local": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.local.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          },
          "defaultConfiguration": ""
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "***-angular:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "***-angular:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "***-angular:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              "src/manifest.webmanifest"
            ],
            "styles": [
              "src/styles.scss",
              "src/assets/css/angular-material-ui.scss"
            ],
            "scripts": []
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "tsconfig.app.json",
              "tsconfig.spec.json",
              "e2e/tsconfig.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "***-angular:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "***-angular:serve:production"
            }
          }
        }
      }
    }
  },
  "defaultProject": "***-angular"
}

package.json:

{
  "name": "***-angular",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular-slider/ngx-slider": "^2.0.4",
    "@angular/animations": "^12.2.16",
    "@angular/common": "~12.2.7",
    "@angular/compiler": "~12.2.7",
    "@angular/core": "~12.2.7",
    "@angular/forms": "~12.2.7",
    "@angular/localize": "~12.2.7",
    "@angular/platform-browser": "~12.2.7",
    "@angular/platform-browser-dynamic": "~12.2.7",
    "@angular/router": "~12.2.7",
    "@angular/service-worker": "~12.2.7",
    "@batch***/batch-autocomplete-lib": "0.0.28",
    "@costlydeveloper/ngx-awesome-popup": "^3.1.3",
    "@ng-bootstrap/ng-bootstrap": "^6.2.0",
    "@ngrx/store": "^12.5.1",
    "@stripe/stripe-js": "^1.29.0",
    "@tailwindcss/forms": "^0.3.3",
    "@tailwindcss/typography": "^0.4.1",
    "angular-datatables": "^13.0.1",
    "angular-google-tag-manager": "1.4.4",
    "angular-mydatepicker": "^0.11.5",
    "apexcharts": "^3.28.3",
    "bootstrap": "^4.4.0",
    "datatables.net": "^1.11.3",
    "datatables.net-dt": "^1.11.3",
    "flatpickr": "^4.6.13",
    "jquery": "^3.6.0",
    "lodash-es": "^4.17.21",
    "moment": "^2.29.3",
    "moment-timezone": "^0.5.40",
    "ng-apexcharts": "^1.5.12",
    "ng2-daterangepicker": "^3.0.1",
    "ngx-daterangepicker-material": "^5.0.1",
    "ngx-mask": "^13.1.12",
    "ngx-pagination": "^6.0.2",
    "ngx-spinner": "^11.0.2",
    "ngx-stripe": "^13.1.0",
    "ngx-toastr": "^14.3.0",
    "popper.js": "^1.16.1",
    "pusher-js": "^7.1.1-beta",
    "rxjs": "~6.5.4",
    "tslib": "^2.0.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~12.2.7",
    "@angular/cli": "~12.2.7",
    "@angular/compiler-cli": "~12.2.7",
    "@angular/language-service": "~12.2.7",
    "@types/datatables.net": "^1.10.21",
    "@types/jasmine": "~3.6.0",
    "@types/jasminewd2": "~2.0.3",
    "@types/jquery": "^3.5.9",
    "@types/node": "^12.11.1",
    "codelyzer": "^6.0.0",
    "jasmine-core": "~3.6.0",
    "jasmine-spec-reporter": "~5.0.0",
    "karma": "~5.0.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "~3.0.2",
    "karma-jasmine": "~4.0.0",
    "karma-jasmine-html-reporter": "^1.5.0",
    "protractor": "~7.0.0",
    "tailwindcss": "^2.2.16",
    "ts-node": "~8.3.0",
    "tslint": "~6.1.0",
    "typescript": "~4.3.5"
  }
}

src/index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>****</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  
  <link rel="manifest" href="manifest.webmanifest">
  <meta name="theme-color" content="#1976d2">
</head>
<!--This is for production enviroment. Make sure this during merging the code-->

<body>
  <app-root></app-root>
  <noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

src/manifest.webmanifest:

{
  "name": "***-angular",
  "short_name": "***-angular",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ]
}

src/app/app.component.ts:

  ngAfterViewInit(): void {
    setTimeout(() => {
      if (this.swUpdate.isEnabled) {
        setInterval(() => {
          this.swUpdate.checkForUpdate();
        }, 3000)

        this.swUpdate.available.subscribe(() => {
          if(confirm("New version available. Load New Version?")) {
            window.location.reload();
          }
        });
      }
    }, 1000)
  }

When a user first arrives on the page, there's a short pause of 1 second. During this time, we check if the service worker is active. This delay is needed to ensure that the subsequent code works properly when we look at the if (this.swUpdate.isEnabled) part.

To make things better, I want to utilize Angular's built-in versionUpdates feature. This would eliminate the need for the 1-second delay and the manual checking that happens every 3 seconds using setInterval. With versionUpdates, the process of detecting updates in the service worker file becomes more straightforward and efficient, making the system work better overall.

The objective is to eliminate the reliance on both the setTimeout and setInterval functions. Despite researching extensively, I haven't found a suitable solution. I'm seeking assistance from anyone who can review the current implementation and provide guidance on whether it's possible to achieve this goal, along with potential approaches. Your insights and assistance in this matter would be greatly appreciated.

Ahsan Hussain
  • 952
  • 4
  • 21
  • 42
  • I would recommend running this directly in your app.module and not in the app.component. You can have an update service that has a signal that gets updated. And checking every 3 seconds is an INSANELY high refresh rate. You should drop that to 5 or 10 MINUTES. Unless there is a critical reason as to why your users absolutely need the latest and greatest within 3 seconds of an update, slow it down a bit. Less overhead for your users and less of a massive hit to your servers when an update rolls out. – Myles Morrone Sep 01 '23 at 02:22
  • -- cont. However you have 2 different ways of doing this. You can use the service idea that just regularly checks for an update every 10 minutes until it finds one and then it notifies the user and stops checking (once there is an update and the user knows, you don't have to keep bugging the server). Or, you could tie the logic into your routing module. Anytime the user navigates to a new page, it runs an update check. More frequent users will be notified sooner than others. Still keep a static check when the app first loads, but both methods work well. – Myles Morrone Sep 01 '23 at 02:25

0 Answers0