[MOBILEAPPS-1701] Custom URL schema issues fixed (#2998)

* Custom URL schema issues

* code implementation changes for checkForMobileAppFlag

* updated test cases

* requested changed addressed

* requested changed addressed

* cspell changes

* review comments addressed
This commit is contained in:
Jatin Chugh 2023-02-20 21:16:09 +05:30 committed by GitHub
parent 472a19b67d
commit 04907df376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 169 additions and 108 deletions

View File

@ -2,82 +2,84 @@
"version": "0.1", "version": "0.1",
"language": "en", "language": "en",
"words": [ "words": [
"Kerberos",
"succes",
"sharedlinks",
"Redistributable",
"fullscreen",
"LGPL",
"mincount",
"QNAME",
"PNAME",
"ADDFEATURES", "ADDFEATURES",
"CHECKIN",
"thumbnailed",
"superadmin",
"SUPERADMIN",
"hruser",
"salesuser",
"networkidle",
"domcontentloaded",
"mimetype",
"ngrx",
"ngstack",
"sidenav",
"injectable",
"iosamw",
"truthy",
"cryptodoc",
"mysites",
"afts", "afts",
"androidamw", "androidamw",
"cardview",
"CHECKIN",
"classlist", "classlist",
"folderlink", "cryptodoc",
"filelink",
"formcontrolname",
"datetimepicker",
"datatable", "datatable",
"repo", "dateitem",
"snackbar", "datetimepicker",
"promisify", "denysvuika",
"xdescribe",
"unfavorite",
"devtools", "devtools",
"gitter",
"jira",
"markdownlint",
"uploader",
"nginx",
"docx", "docx",
"SOLR", "domcontentloaded",
"simpletask", "errored",
"titlecase", "erroredSpy",
"exif",
"unshare", "filelink",
"qshare", "folderlink",
"validators", "formcontrolname",
"fullscreen",
"gitter",
"guid", "guid",
"hruser",
"injectable",
"iosamw",
"jira",
"jsonp",
"Kerberos",
"keycodes",
"LGPL",
"markdownlint",
"mimetype",
"mincount",
"mobileapps",
"mysites",
"networkidle",
"nginx",
"ngrx",
"ngstack",
"noderef",
"pdfjs",
"PNAME",
"polyfill", "polyfill",
"polyfills", "polyfills",
"jsonp", "promisify",
"pdfjs", "QNAME",
"xpath", "qshare",
"tooltip", "Redistributable",
"tooltips", "repo",
"unindent", "salesuser",
"exif", "sharedlinks",
"cardview", "sidenav",
"webm", "simpletask",
"keycodes", "snackbar",
"denysvuika", "SOLR",
"submenu", "submenu",
"submenus", "submenus",
"dateitem", "succes",
"superadmin",
"thumbnailed",
"titlecase",
"tooltip",
"tooltips",
"truthy",
"unfavorite",
"unindent",
"unshare",
"uploader",
"validators",
"versionable", "versionable",
"erroredSpy", "webm",
"errored", "xdescribe",
"noderef" "xpath"
], ],
"dictionaries": ["html", "en-gb", "en_US"] "dictionaries": [
"html",
"en-gb",
"en_US"
]
} }

View File

@ -1,8 +1,8 @@
<div class="container"> <div class="container">
<button mat-button (click)="openInApp()" data-automation-id="open-in-app-button"> <button mat-button (click)="openInApp()" data-automation-id="open-in-app-button" class="open-in-app">
<span>{{ 'APP.DIALOGS.MOBILE_APP.MOBILE_APP_BUTTON_LABEL' }}</span> <span>{{ 'APP.DIALOGS.MOBILE_APP.MOBILE_APP_BUTTON_LABEL' | translate }}</span>
</button> </button>
<button mat-button mat-dialog-close> <button mat-button (click)="onCloseDialog()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</div> </div>

View File

@ -6,8 +6,12 @@
} }
.mat-dialog-container{ .mat-dialog-container{
padding: 12px; padding: 5px;
border-radius: 36px; border-radius: 36px;
background-color: var(--theme-blue-button-color); background-color: var(--theme-blue-button-color);
color: var(--theme-about-panel-background-color); color: var(--theme-about-panel-background-color);
} }
.open-in-app.mat-button {
overflow-x: hidden;
}

View File

@ -1,16 +1,29 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { OpenInAppComponent } from './open-in-app.component'; import { OpenInAppComponent } from './open-in-app.component';
import { initialState, LibTestingModule } from '../../testing/lib-testing-module';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core';
describe('OpenInAppComponent', () => { describe('OpenInAppComponent', () => {
let fixture: ComponentFixture<OpenInAppComponent>; let fixture: ComponentFixture<OpenInAppComponent>;
let component: OpenInAppComponent; let component: OpenInAppComponent;
const mockDialogRef = {
close: jasmine.createSpy('close'),
open: jasmine.createSpy('open')
};
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [OpenInAppComponent], declarations: [OpenInAppComponent],
providers: [{ provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } }] imports: [LibTestingModule, TranslateModule],
providers: [
provideMockStore({ initialState }),
{ provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } },
{ provide: MatDialogRef, useValue: mockDialogRef }
]
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(OpenInAppComponent); fixture = TestBed.createComponent(OpenInAppComponent);
@ -35,4 +48,11 @@ describe('OpenInAppComponent', () => {
expect(currentLocation).toBe('mockRedirectUrl'); expect(currentLocation).toBe('mockRedirectUrl');
}); });
it('should set the value `mobile_notification_expires_in` in session storage on dialog close', async () => {
sessionStorage.clear();
component.onCloseDialog();
expect(sessionStorage.getItem('mobile_notification_expires_in')).not.toBeNull();
expect(mockDialogRef.close).toHaveBeenCalled();
});
}); });

View File

@ -24,7 +24,7 @@
*/ */
import { Component, Inject, ViewEncapsulation } from '@angular/core'; import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
export interface OpenInAppDialogOptions { export interface OpenInAppDialogOptions {
redirectUrl: string; redirectUrl: string;
@ -41,7 +41,8 @@ export class OpenInAppComponent {
constructor( constructor(
@Inject(MAT_DIALOG_DATA) @Inject(MAT_DIALOG_DATA)
public data: OpenInAppDialogOptions public data: OpenInAppDialogOptions,
private dialog: MatDialogRef<OpenInAppComponent>
) { ) {
if (data) { if (data) {
this.redirectUrl = data.redirectUrl; this.redirectUrl = data.redirectUrl;
@ -51,4 +52,10 @@ export class OpenInAppComponent {
openInApp(): void { openInApp(): void {
this.window.location.href = this.redirectUrl; this.window.location.href = this.redirectUrl;
} }
onCloseDialog(): void {
const time: number = new Date().getTime();
sessionStorage.setItem('mobile_notification_expires_in', time.toString());
this.dialog.close();
}
} }

View File

@ -60,7 +60,7 @@ describe('AcaMobileAppSwitcherService', () => {
spyOnProperty(window.navigator, 'userAgent').and.returnValue('iphone'); spyOnProperty(window.navigator, 'userAgent').and.returnValue('iphone');
const url: string = window.location.href; const url: string = window.location.href;
const iphoneUrl: string = appConfig.config.mobileAppSwitch.iphoneUrl + url; const iphoneUrl: string = appConfig.config.mobileAppSwitch.iphoneUrl + url;
service.showAppNotification(); service.identifyBrowserAndSetRedirectURL();
expect(service.redirectUrl).toEqual(iphoneUrl); expect(service.redirectUrl).toEqual(iphoneUrl);
}); });
@ -68,38 +68,49 @@ describe('AcaMobileAppSwitcherService', () => {
spyOnProperty(window.navigator, 'userAgent').and.returnValue('android'); spyOnProperty(window.navigator, 'userAgent').and.returnValue('android');
const url: string = window.location.href; const url: string = window.location.href;
const androidUrl: string = appConfig.config.mobileAppSwitch.androidUrlPart1 + url + appConfig.config.mobileAppSwitch.androidUrlPart2; const androidUrl: string = appConfig.config.mobileAppSwitch.androidUrlPart1 + url + appConfig.config.mobileAppSwitch.androidUrlPart2;
service.showAppNotification(); service.identifyBrowserAndSetRedirectURL();
expect(service.redirectUrl).toEqual(androidUrl); expect(service.redirectUrl).toEqual(androidUrl);
}); });
it('should check if `showAppNotification` function is called', () => { it('should check if `identifyBrowserAndSetRedirectURL` function is called', () => {
const showAppNotificationSpy: jasmine.Spy<() => void> = spyOn(service, 'showAppNotification'); const identifyBrowserAndSetRedirectURLSpy: jasmine.Spy<() => void> = spyOn(service, 'identifyBrowserAndSetRedirectURL');
service.checkForMobileApp(); service.resolveExistenceOfDialog();
expect(showAppNotificationSpy).toHaveBeenCalled(); expect(identifyBrowserAndSetRedirectURLSpy).toHaveBeenCalled();
}); });
it('should not display `openInApp` dialog box when timeDifference is less than the session time', () => { it('should not display `openInApp` dialog box after closing the same and time difference less than session time', () => {
service.checkForMobileApp(); const time: number = new Date().getTime();
const showAppNotificationSpy: jasmine.Spy<() => void> = spyOn(service, 'showAppNotification'); sessionStorage.setItem('mobile_notification_expires_in', time.toString());
service.checkForMobileApp(); const identifyBrowserAndSetRedirectURLSpy: jasmine.Spy<() => void> = spyOn(service, 'identifyBrowserAndSetRedirectURL');
expect(showAppNotificationSpy).not.toHaveBeenCalled(); service.verifySessionExistsForDialog();
expect(identifyBrowserAndSetRedirectURLSpy).not.toHaveBeenCalled();
}); });
it('should check if `openInApp` dialog box is getting opened with `iphone` url', () => { it('should check if `openInApp` dialog box is getting opened with `iphone` url', () => {
const openInAppSpy: jasmine.Spy<(redirectUrl: string) => void> = spyOn(service, 'openInApp');
const url: string = window.location.href; const url: string = window.location.href;
service.redirectUrl = appConfig.config.mobileAppSwitch.iphoneUrl + url; service.redirectUrl = appConfig.config.mobileAppSwitch.iphoneUrl + url;
service.showAppNotification(); service.identifyBrowserAndSetRedirectURL();
expect(openInAppSpy).toHaveBeenCalled();
expect(mockDialogRef.open).toHaveBeenCalled(); expect(mockDialogRef.open).toHaveBeenCalled();
}); });
it('should check if `openInApp` dialog box is getting opened with `android` url', () => { it('should check if `openInApp` dialog box is getting opened with `android` url', () => {
const openInAppSpy: jasmine.Spy<(redirectUrl: string) => void> = spyOn(service, 'openInApp');
const url: string = window.location.href; const url: string = window.location.href;
service.redirectUrl = appConfig.config.mobileAppSwitch.androidUrlPart1 + url + appConfig.config.mobileAppSwitch.androidUrlPart2; service.redirectUrl = appConfig.config.mobileAppSwitch.androidUrlPart1 + url + appConfig.config.mobileAppSwitch.androidUrlPart2;
service.showAppNotification(); service.identifyBrowserAndSetRedirectURL();
expect(openInAppSpy).toHaveBeenCalled();
expect(mockDialogRef.open).toHaveBeenCalled(); expect(mockDialogRef.open).toHaveBeenCalled();
}); });
it('should not display Open in app dialog when the web application is opened within mobile application', () => {
spyOn(service, 'getCurrentUrl').and.returnValue('localhost?mobileapps=true');
const verifySessionExistsForDialogSpy: jasmine.Spy<() => void> = spyOn(service, 'verifySessionExistsForDialog');
service.resolveExistenceOfDialog();
expect(verifySessionExistsForDialogSpy).not.toHaveBeenCalled();
});
it('should display Open in app dialog when the web application is opened in mobile browser', () => {
spyOn(service, 'getCurrentUrl').and.returnValue('localhost');
const verifySessionExistsForDialogSpy: jasmine.Spy<() => void> = spyOn(service, 'verifySessionExistsForDialog');
service.resolveExistenceOfDialog();
expect(verifySessionExistsForDialogSpy).toHaveBeenCalled();
});
}); });

View File

@ -46,31 +46,42 @@ export class AcaMobileAppSwitcherService {
this.mobileAppSwitchConfig = this.config.get<MobileAppSwitchConfigurationOptions>('mobileAppSwitch'); this.mobileAppSwitchConfig = this.config.get<MobileAppSwitchConfigurationOptions>('mobileAppSwitch');
} }
checkForMobileApp(): void { resolveExistenceOfDialog(): void {
const currentTime: number = new Date().getTime(); const url: string = this.getCurrentUrl();
const sessionTime: string = sessionStorage.getItem('mobile_notification_expires_in'); const queryParams: string = url.split('?')[1];
let queryParamsList = [];
let hideBanner = false;
if (queryParams !== null && queryParams !== undefined) {
queryParamsList = queryParams.split('&');
hideBanner = queryParamsList.some((param) => param.split('=')[0] === 'mobileapps' && param.split('=')[1] === 'true');
}
if (!hideBanner) {
this.verifySessionExistsForDialog();
}
}
verifySessionExistsForDialog(): void {
const sessionTime: string = sessionStorage.getItem('mobile_notification_expires_in');
if (sessionTime !== null) { if (sessionTime !== null) {
const currentTime: number = new Date().getTime();
const sessionConvertedTime: number = parseFloat(sessionTime); const sessionConvertedTime: number = parseFloat(sessionTime);
const timeDifference: number = (currentTime - sessionConvertedTime) / (1000 * 60 * 60); const timeDifference: number = (currentTime - sessionConvertedTime) / (1000 * 60 * 60);
const sessionTimeForOpenAppDialogDisplay: number = parseFloat(this.mobileAppSwitchConfig.sessionTimeForOpenAppDialogDisplay); const sessionTimeForOpenAppDialogDisplay: number = parseFloat(this.mobileAppSwitchConfig.sessionTimeForOpenAppDialogDisplay);
if (timeDifference > sessionTimeForOpenAppDialogDisplay) { if (timeDifference > sessionTimeForOpenAppDialogDisplay) {
this.showAppNotification(); this.clearSessionExpireTime();
this.identifyBrowserAndSetRedirectURL();
} }
} else { } else {
this.showAppNotification(); this.identifyBrowserAndSetRedirectURL();
} }
} }
showAppNotification(): void { identifyBrowserAndSetRedirectURL(): void {
const ua: string = navigator.userAgent.toLowerCase(); const ua: string = navigator.userAgent.toLowerCase();
const isAndroid: boolean = ua.indexOf('android') > -1; const isAndroid: boolean = ua.indexOf('android') > -1;
const isIOS: boolean = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1; const isIOS: boolean = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1;
const currentUrl: string = window.location.href; const currentUrl: string = this.getCurrentUrl();
const time: number = new Date().getTime();
sessionStorage.setItem('mobile_notification_expires_in', time.toString());
if (isIOS === true) { if (isIOS === true) {
this.redirectUrl = this.mobileAppSwitchConfig.iphoneUrl + currentUrl; this.redirectUrl = this.mobileAppSwitchConfig.iphoneUrl + currentUrl;
@ -79,22 +90,27 @@ export class AcaMobileAppSwitcherService {
} }
if (this.redirectUrl !== undefined && this.redirectUrl !== null) { if (this.redirectUrl !== undefined && this.redirectUrl !== null) {
this.openInApp(this.redirectUrl); this.openDialog(this.redirectUrl);
} }
} }
openInApp(redirectUrl: string): void { openDialog(redirectUrl: string): void {
this.dialog.open(OpenInAppComponent, { this.dialog.open(OpenInAppComponent, {
data: { data: {
redirectUrl redirectUrl
}, },
width: '75%', hasBackdrop: false,
width: 'auto',
role: 'dialog', role: 'dialog',
position: { bottom: '50px' } position: { bottom: '20px' }
}); });
} }
reset(): void { clearSessionExpireTime(): void {
sessionStorage.removeItem('mobile_notification_expires_in'); sessionStorage.removeItem('mobile_notification_expires_in');
} }
getCurrentUrl(): string {
return window.location.href;
}
} }

View File

@ -182,9 +182,9 @@ export class AppService implements OnDestroy {
const isMobileSwitchEnabled: boolean = this.config.get<boolean>('mobileAppSwitch.enabled', false); const isMobileSwitchEnabled: boolean = this.config.get<boolean>('mobileAppSwitch.enabled', false);
if (isMobileSwitchEnabled) { if (isMobileSwitchEnabled) {
this.acaMobileAppSwitcherService.checkForMobileApp(); this.acaMobileAppSwitcherService.resolveExistenceOfDialog();
} else { } else {
this.acaMobileAppSwitcherService.reset(); this.acaMobileAppSwitcherService.clearSessionExpireTime();
} }
} }

View File

@ -32,10 +32,11 @@ import { OpenInAppComponent } from './components/open-in-app/open-in-app.compone
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({ @NgModule({
imports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule], imports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule, TranslateModule],
exports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule], exports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule, TranslateModule],
declarations: [OpenInAppComponent] declarations: [OpenInAppComponent]
}) })
export class SharedModule { export class SharedModule {