diff --git a/app/src/app.config.json.tpl b/app/src/app.config.json.tpl index 49333691b5..527e3714d4 100644 --- a/app/src/app.config.json.tpl +++ b/app/src/app.config.json.tpl @@ -11,7 +11,8 @@ "iphoneUrl": "iosamw://", "androidUrlPart1": "intent:///", "androidUrlPart2": "#Intent;scheme=androidamw;package=com.alfresco.content.app;end", - "sessionTimeForOpenAppDialogDisplay": "${APP_CONFIG_SESSION_TIME_FOR_OPEN_APP_DIALOG_DISPLAY_IN_HOURS}" + "sessionTimeForOpenAppDialogDisplay": "${APP_CONFIG_SESSION_TIME_FOR_OPEN_APP_DIALOG_DISPLAY_IN_HOURS}", + "appStoreUrl": "https://apps.apple.com/us/app/alfresco-mobile-workspace/id1514434480" }, "plugins": { "aosPlugin": ${APP_CONFIG_PLUGIN_AOS}, diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index e418533f75..52e89999f7 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -286,7 +286,9 @@ "NO_LABEL": "Cancel" }, "MOBILE_APP": { - "MOBILE_APP_BUTTON_LABEL": "Open in App" + "MOBILE_APP_BUTTON_LABEL": "Open in App", + "DOWNLOAD_APP_BUTTON_LABEL": "Don't have the app? Download now.", + "OPEN_ALFRESCO_MOBILE_APP": "Open using Alfresco Mobile application?" } }, "DOCUMENT_LIST": { diff --git a/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.spec.ts b/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.spec.ts index 9ef8f8e3db..f2ef538f5a 100644 --- a/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.spec.ts +++ b/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.spec.ts @@ -30,7 +30,7 @@ import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { SetSelectedNodesAction } from '@alfresco/aca-shared/store'; -import { AppExtensionService } from '@alfresco/aca-shared'; +import { AppExtensionService, AppService } from '@alfresco/aca-shared'; describe('SharedLinkViewComponent', () => { let component: SharedLinkViewComponent; @@ -41,6 +41,9 @@ describe('SharedLinkViewComponent', () => { dispatch: jasmine.createSpy('dispatch'), select: () => of({}) }; + const appServiceMock = { + openMobileAppDialog: () => {} + }; beforeEach(() => { TestBed.configureTestingModule({ @@ -55,6 +58,10 @@ describe('SharedLinkViewComponent', () => { snapshot: { data: { preferencePrefix: 'prefix' } }, params: of({ id: '123' }) } + }, + { + provide: AppService, + useValue: appServiceMock } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.ts b/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.ts index 5e0f2faf85..07ce9b2aa5 100644 --- a/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.ts +++ b/projects/aca-content/src/lib/components/shared-link-view/shared-link-view.component.ts @@ -31,7 +31,7 @@ import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { forkJoin, from, of, Subject } from 'rxjs'; import { catchError, mergeMap, takeUntil } from 'rxjs/operators'; -import { AppExtensionService } from '@alfresco/aca-shared'; +import { AppExtensionService, AppService } from '@alfresco/aca-shared'; @Component({ selector: 'app-shared-link-view', @@ -50,7 +50,8 @@ export class SharedLinkViewComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private store: Store, private extensions: AppExtensionService, - private alfrescoApiService: AlfrescoApiService + private alfrescoApiService: AlfrescoApiService, + private appService: AppService ) { this.sharedLinksApi = new SharedlinksApi(this.alfrescoApiService.getInstance()); } @@ -65,6 +66,7 @@ export class SharedLinkViewComponent implements OnInit, OnDestroy { .subscribe(([sharedEntry, sharedId]: [SharedLinkEntry, string]) => { if (sharedEntry) { this.store.dispatch(new SetSelectedNodesAction([sharedEntry as any])); + this.appService.openMobileAppDialog(); } this.sharedLinkId = sharedId; }); diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.html b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.html index 4433ab6b3a..8c814acd01 100644 --- a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.html +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.html @@ -1,8 +1,18 @@ -
- +
+ +
+ -
+ +
+
diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.scss b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.scss index 7dcba44cdd..36cf82b28b 100644 --- a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.scss +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.scss @@ -1,21 +1,64 @@ -.container{ - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; -} +aca-open-in-app { + .open-in-app-container { + display: flex; + place-content: center; + padding: 0; + border-radius: 8px; + background-color: var(--theme-primary-color); + color: var(--theme-about-panel-background-color); + margin-top: 12px; + } -.mat-dialog-container{ - padding: 5px; - border-radius: 36px; - background-color: var(--theme-blue-button-color); - color: var(--theme-about-panel-background-color); -} + .open-in-app { + overflow-x: hidden; + font-size: 16px; + width: 100%; + padding: 0; + height: 48px; -.open-in-app.mat-button { - overflow-x: hidden; -} + &:focus-visible { + outline: none; + border-radius: unset; + } + } + + .download-app-container { + display: flex; + place-content: center; + margin-top: 12px; + } + + .alfresco-mobile-application-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-size: 14px; + padding: 6px 0; + } + + .cross-button { + padding-right: 0; + + &:focus-visible { + outline: none; + border-radius: unset; + } + } + + .cross-icon { + font-weight: bold; + font-size: 21px; + height: 21px; + } + + .download-app-button { + background: var(--theme-dialog-background-color); + color: var(--theme-primary-color); + font-size: 14px; + } -.open-in-app.mat-button.cdk-program-focused .mat-button-focus-overlay { - opacity: 0; + .mat-dialog-container { + padding: 8px 24px 24px; + } } diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts index 00998d45ca..f52685ff5d 100644 --- a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.spec.ts @@ -45,7 +45,7 @@ describe('OpenInAppComponent', () => { imports: [LibTestingModule, SharedModule.forRoot(), MatIconTestingModule], providers: [ provideMockStore({ initialState }), - { provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl' } }, + { provide: MAT_DIALOG_DATA, useValue: { redirectUrl: 'mockRedirectUrl', appStoreUrl: 'mockAppStoreUrl' } }, { provide: MatDialogRef, useValue: mockDialogRef } ] }); @@ -78,4 +78,22 @@ describe('OpenInAppComponent', () => { expect(sessionStorage.getItem('mobile_notification_expires_in')).not.toBeNull(); expect(mockDialogRef.close).toHaveBeenCalled(); }); + + it('should redirect to App Store for downloading the app in case of Ios device', async () => { + let currentLocation: string | string[]; + const windowStub: Window & typeof globalThis = { + location: { + set href(value: string | string[]) { + currentLocation = value; + } + } + } as Window & typeof globalThis; + component.window = windowStub; + const downloadAppButton = fixture.debugElement.query(By.css('[data-automation-id="download-app-button"]')).nativeElement; + downloadAppButton.dispatchEvent(new Event('click')); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(currentLocation).toBe('mockAppStoreUrl'); + }); }); diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.ts b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.ts index 873669eff7..4cab112580 100644 --- a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.ts +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.component.ts @@ -27,6 +27,7 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; export interface OpenInAppDialogOptions { redirectUrl: string; + appStoreUrl: string; } @Component({ selector: 'aca-open-in-app', @@ -35,7 +36,8 @@ export interface OpenInAppDialogOptions { encapsulation: ViewEncapsulation.None }) export class OpenInAppComponent { - private readonly redirectUrl: string; + private redirectUrl: string; + public appStoreUrl: string; public window: Window & typeof globalThis = window; constructor( @@ -45,6 +47,7 @@ export class OpenInAppComponent { ) { if (data) { this.redirectUrl = data.redirectUrl; + this.appStoreUrl = data.appStoreUrl; } } @@ -52,6 +55,10 @@ export class OpenInAppComponent { this.window.location.href = this.redirectUrl; } + downloadIosApp(): void { + this.window.location.href = this.appStoreUrl; + } + onCloseDialog(): void { const time: number = new Date().getTime(); sessionStorage.setItem('mobile_notification_expires_in', time.toString()); diff --git a/projects/aca-shared/src/lib/components/open-in-app/open-in-app.module.ts b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.module.ts new file mode 100644 index 0000000000..97ac934ad6 --- /dev/null +++ b/projects/aca-shared/src/lib/components/open-in-app/open-in-app.module.ts @@ -0,0 +1,38 @@ +/*! + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { BrowserModule } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; +import { OpenInAppComponent } from './open-in-app.component'; + +@NgModule({ + imports: [CommonModule, MatIconModule, TranslateModule, MatButtonModule, BrowserModule], + declarations: [OpenInAppComponent], + exports: [OpenInAppComponent] +}) +export class OpenInAppModule {} diff --git a/projects/aca-shared/src/lib/services/aca-mobile-app-switcher.service.ts b/projects/aca-shared/src/lib/services/aca-mobile-app-switcher.service.ts index 27ac08831a..38623ba0f2 100644 --- a/projects/aca-shared/src/lib/services/aca-mobile-app-switcher.service.ts +++ b/projects/aca-shared/src/lib/services/aca-mobile-app-switcher.service.ts @@ -24,7 +24,7 @@ import { AppConfigService } from '@alfresco/adf-core'; import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { OpenInAppComponent } from '../components/open-in-app/open-in-app.component'; export interface MobileAppSwitchConfigurationOptions { @@ -33,6 +33,7 @@ export interface MobileAppSwitchConfigurationOptions { androidUrlPart1: string; androidUrlPart2: string; sessionTimeForOpenAppDialogDisplay: string; + appStoreUrl: string; } @Injectable({ providedIn: 'root' @@ -40,6 +41,8 @@ export interface MobileAppSwitchConfigurationOptions { export class AcaMobileAppSwitcherService { private mobileAppSwitchConfig: MobileAppSwitchConfigurationOptions; public redirectUrl: string; + public appStoreUrl: string; + private dialogRef: MatDialogRef; constructor(private config: AppConfigService, private dialog: MatDialog) { this.mobileAppSwitchConfig = this.config.get('mobileAppSwitch'); @@ -79,30 +82,35 @@ export class AcaMobileAppSwitcherService { identifyBrowserAndSetRedirectURL(): void { const ua: string = navigator.userAgent.toLowerCase(); const isAndroid: boolean = ua.indexOf('android') > -1; - const isIOS: boolean = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1; + const isIPadInSafari = /Macintosh/i.test(ua) && navigator.maxTouchPoints && navigator.maxTouchPoints > 1; + const isIOS: boolean = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1 || isIPadInSafari; const currentUrl: string = this.getCurrentUrl(); if (isIOS === true) { this.redirectUrl = this.mobileAppSwitchConfig.iphoneUrl + currentUrl; + this.appStoreUrl = this.mobileAppSwitchConfig.appStoreUrl; } else if (isAndroid === true) { this.redirectUrl = this.mobileAppSwitchConfig.androidUrlPart1 + currentUrl + this.mobileAppSwitchConfig.androidUrlPart2; } if (this.redirectUrl !== undefined && this.redirectUrl !== null) { - this.openDialog(this.redirectUrl); + this.openDialog(this.redirectUrl, this.appStoreUrl); } } - openDialog(redirectUrl: string): void { - this.dialog.open(OpenInAppComponent, { - data: { - redirectUrl - }, - hasBackdrop: false, - width: 'auto', - role: 'dialog', - position: { bottom: '20px' } - }); + openDialog(redirectUrl: string, appStoreUrl?: string): void { + if (!this.dialogRef) { + this.dialogRef = this.dialog.open(OpenInAppComponent, { + data: { + redirectUrl, + appStoreUrl + }, + hasBackdrop: false, + width: '100%', + role: 'dialog', + position: { bottom: '0' } + }); + } } clearSessionExpireTime(): void { @@ -112,4 +120,11 @@ export class AcaMobileAppSwitcherService { getCurrentUrl(): string { return window.location.href; } + + closeDialog(): void { + if (this.dialogRef) { + this.dialog.closeAll(); + this.dialogRef = null; + } + } } diff --git a/projects/aca-shared/src/lib/services/app.service.ts b/projects/aca-shared/src/lib/services/app.service.ts index 51b62df2dd..25171b21f9 100644 --- a/projects/aca-shared/src/lib/services/app.service.ts +++ b/projects/aca-shared/src/lib/services/app.service.ts @@ -103,6 +103,8 @@ export class AppService implements OnDestroy { this.authenticationService.onLogout.subscribe(() => { searchQueryBuilderService.resetToDefaults(); + acaMobileAppSwitcherService.clearSessionExpireTime(); + acaMobileAppSwitcherService.closeDialog(); }); this.pageHeading$ = this.router.events.pipe( @@ -164,17 +166,11 @@ export class AppService implements OnDestroy { if (isReady) { this.loadRepositoryStatus(); this.loadUserProfile(); + this.openMobileAppDialog(); } }); this.overlayContainer.getContainerElement().setAttribute('role', 'region'); - - const isMobileSwitchEnabled: boolean = this.config.get('mobileAppSwitch.enabled', false); - if (isMobileSwitchEnabled) { - this.acaMobileAppSwitcherService.resolveExistenceOfDialog(); - } else { - this.acaMobileAppSwitcherService.clearSessionExpireTime(); - } } private loadRepositoryStatus() { @@ -266,4 +262,13 @@ export class AppService implements OnDestroy { cssLinkElement.setAttribute('href', url); document.head.appendChild(cssLinkElement); } + + public openMobileAppDialog(): void { + const isMobileSwitchEnabled: boolean = this.config.get('mobileAppSwitch.enabled', false); + if (isMobileSwitchEnabled) { + this.acaMobileAppSwitcherService.resolveExistenceOfDialog(); + } else { + this.acaMobileAppSwitcherService.clearSessionExpireTime(); + } + } } diff --git a/projects/aca-shared/src/lib/shared.module.ts b/projects/aca-shared/src/lib/shared.module.ts index fdd0b9ba50..dd0a223972 100644 --- a/projects/aca-shared/src/lib/shared.module.ts +++ b/projects/aca-shared/src/lib/shared.module.ts @@ -27,7 +27,6 @@ import { ContentApiService } from './services/content-api.service'; import { NodePermissionService } from './services/node-permission.service'; import { AppService } from './services/app.service'; import { ContextActionsModule } from './directives/contextmenu/contextmenu.module'; -import { OpenInAppComponent } from './components/open-in-app/open-in-app.component'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatDialogModule } from '@angular/material/dialog'; @@ -35,8 +34,7 @@ import { TranslateModule } from '@ngx-translate/core'; @NgModule({ imports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule, TranslateModule], - exports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule, TranslateModule], - declarations: [OpenInAppComponent] + exports: [ContextActionsModule, MatButtonModule, MatIconModule, MatDialogModule, TranslateModule] }) export class SharedModule { static forRoot(): ModuleWithProviders { diff --git a/projects/aca-shared/src/public-api.ts b/projects/aca-shared/src/public-api.ts index b54edf65cf..6080bd6bcf 100644 --- a/projects/aca-shared/src/public-api.ts +++ b/projects/aca-shared/src/public-api.ts @@ -41,6 +41,8 @@ export * from './lib/components/info-drawer/info-drawer.component'; export * from './lib/components/info-drawer/shared-info-drawer.module'; export * from './lib/components/document-base-page/document-base-page.component'; export * from './lib/components/document-base-page/document-base-page.service'; +export * from './lib/components/open-in-app/open-in-app.component'; +export * from './lib/components/open-in-app/open-in-app.module'; export * from './lib/directives/contextmenu/contextmenu.directive'; export * from './lib/directives/contextmenu/contextmenu.module';