Commit bbf78721a7436f91cf761673f3894d88df27895a

Authored by Igor Kulikov
1 parent ed955510

Improve mobile app support

@@ -152,6 +152,7 @@ export interface IStateController { @@ -152,6 +152,7 @@ export interface IStateController {
152 getStateIndex(): number; 152 getStateIndex(): number;
153 getStateIdAtIndex(index: number): string; 153 getStateIdAtIndex(index: number): string;
154 getEntityId(entityParamName: string): EntityId; 154 getEntityId(entityParamName: string): EntityId;
  155 + getCurrentStateName(): string;
155 } 156 }
156 157
157 export interface SubscriptionInfo { 158 export interface SubscriptionInfo {
@@ -45,6 +45,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions' @@ -45,6 +45,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions'
45 import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 45 import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
46 import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; 46 import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
47 import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; 47 import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
  48 +import { isMobileApp } from '@core/utils';
48 49
49 @Injectable({ 50 @Injectable({
50 providedIn: 'root' 51 providedIn: 'root'
@@ -194,11 +195,13 @@ export class AuthService { @@ -194,11 +195,13 @@ export class AuthService {
194 } 195 }
195 196
196 public gotoDefaultPlace(isAuthenticated: boolean) { 197 public gotoDefaultPlace(isAuthenticated: boolean) {
197 - const authState = getCurrentAuthState(this.store);  
198 - const url = this.defaultUrl(isAuthenticated, authState);  
199 - this.zone.run(() => {  
200 - this.router.navigateByUrl(url);  
201 - }); 198 + if (!isMobileApp()) {
  199 + const authState = getCurrentAuthState(this.store);
  200 + const url = this.defaultUrl(isAuthenticated, authState);
  201 + this.zone.run(() => {
  202 + this.router.navigateByUrl(url);
  203 + });
  204 + }
202 } 205 }
203 206
204 public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> { 207 public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> {
@@ -516,12 +519,15 @@ export class AuthService { @@ -516,12 +519,15 @@ export class AuthService {
516 return this.refreshTokenSubject !== null; 519 return this.refreshTokenSubject !== null;
517 } 520 }
518 521
519 - public setUserFromJwtToken(jwtToken, refreshToken, notify) { 522 + public setUserFromJwtToken(jwtToken, refreshToken, notify): Observable<boolean> {
  523 + const authenticatedSubject = new ReplaySubject<boolean>();
520 if (!jwtToken) { 524 if (!jwtToken) {
521 AuthService.clearTokenData(); 525 AuthService.clearTokenData();
522 if (notify) { 526 if (notify) {
523 this.notifyUnauthenticated(); 527 this.notifyUnauthenticated();
524 } 528 }
  529 + authenticatedSubject.next(false);
  530 + authenticatedSubject.complete();
525 } else { 531 } else {
526 this.updateAndValidateTokens(jwtToken, refreshToken, true); 532 this.updateAndValidateTokens(jwtToken, refreshToken, true);
527 if (notify) { 533 if (notify) {
@@ -530,16 +536,30 @@ export class AuthService { @@ -530,16 +536,30 @@ export class AuthService {
530 (authPayload) => { 536 (authPayload) => {
531 this.notifyUserLoaded(true); 537 this.notifyUserLoaded(true);
532 this.notifyAuthenticated(authPayload); 538 this.notifyAuthenticated(authPayload);
  539 + authenticatedSubject.next(true);
  540 + authenticatedSubject.complete();
533 }, 541 },
534 () => { 542 () => {
535 this.notifyUserLoaded(true); 543 this.notifyUserLoaded(true);
536 this.notifyUnauthenticated(); 544 this.notifyUnauthenticated();
  545 + authenticatedSubject.next(false);
  546 + authenticatedSubject.complete();
537 } 547 }
538 ); 548 );
539 } else { 549 } else {
540 - this.loadUser(false).subscribe(); 550 + this.loadUser(false).subscribe(
  551 + () => {
  552 + authenticatedSubject.next(true);
  553 + authenticatedSubject.complete();
  554 + },
  555 + () => {
  556 + authenticatedSubject.next(false);
  557 + authenticatedSubject.complete();
  558 + }
  559 + );
541 } 560 }
542 } 561 }
  562 + return authenticatedSubject;
543 } 563 }
544 564
545 private updateAndValidateTokens(jwtToken, refreshToken, notify: boolean) { 565 private updateAndValidateTokens(jwtToken, refreshToken, notify: boolean) {
@@ -29,6 +29,7 @@ import { DialogService } from '@core/services/dialog.service'; @@ -29,6 +29,7 @@ import { DialogService } from '@core/services/dialog.service';
29 import { TranslateService } from '@ngx-translate/core'; 29 import { TranslateService } from '@ngx-translate/core';
30 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
31 import { isObject } from '@core/utils'; 31 import { isObject } from '@core/utils';
  32 +import { MobileService } from '@core/services/mobile.service';
32 33
33 @Injectable({ 34 @Injectable({
34 providedIn: 'root' 35 providedIn: 'root'
@@ -41,6 +42,7 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -41,6 +42,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
41 private dialogService: DialogService, 42 private dialogService: DialogService,
42 private utils: UtilsService, 43 private utils: UtilsService,
43 private translate: TranslateService, 44 private translate: TranslateService,
  45 + private mobileService: MobileService,
44 private zone: NgZone) {} 46 private zone: NgZone) {}
45 47
46 getAuthState(): Observable<AuthState> { 48 getAuthState(): Observable<AuthState> {
@@ -108,6 +110,10 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -108,6 +110,10 @@ export class AuthGuard implements CanActivate, CanActivateChild {
108 return of(false); 110 return of(false);
109 } 111 }
110 } 112 }
  113 + if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) {
  114 + this.mobileService.handleMobileNavigation(path, params);
  115 + return of(false);
  116 + }
111 const defaultUrl = this.authService.defaultUrl(true, authState, path, params); 117 const defaultUrl = this.authService.defaultUrl(true, authState, path, params);
112 if (defaultUrl) { 118 if (defaultUrl) {
113 // this.authService.gotoDefaultPlace(true); 119 // this.authService.gotoDefaultPlace(true);
@@ -20,9 +20,14 @@ import { isDefined } from '@core/utils'; @@ -20,9 +20,14 @@ import { isDefined } from '@core/utils';
20 import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models'; 20 import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models';
21 import { from, of } from 'rxjs'; 21 import { from, of } from 'rxjs';
22 import { Observable } from 'rxjs/internal/Observable'; 22 import { Observable } from 'rxjs/internal/Observable';
23 -import { catchError } from 'rxjs/operators'; 23 +import { catchError, tap } from 'rxjs/operators';
  24 +import { OpenDashboardMessage, ReloadUserMessage, WindowMessage } from '@shared/models/window-message.model';
  25 +import { Params, Router } from '@angular/router';
  26 +import { AuthService } from '@core/auth/auth.service';
24 27
25 const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler'; 28 const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler';
  29 +const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler';
  30 +const navigationHandler = 'tbMobileNavigationHandler';
26 const mobileHandler = 'tbMobileHandler'; 31 const mobileHandler = 'tbMobileHandler';
27 32
28 // @dynamic 33 // @dynamic
@@ -34,10 +39,20 @@ export class MobileService { @@ -34,10 +39,20 @@ export class MobileService {
34 private readonly mobileApp; 39 private readonly mobileApp;
35 private readonly mobileChannel; 40 private readonly mobileChannel;
36 41
37 - constructor(@Inject(WINDOW) private window: Window) { 42 + private readonly onWindowMessageListener = this.onWindowMessage.bind(this);
  43 +
  44 + private reloadUserObservable: Observable<boolean>;
  45 + private lastDashboardId: string;
  46 +
  47 + constructor(@Inject(WINDOW) private window: Window,
  48 + private router: Router,
  49 + private authService: AuthService) {
38 const w = (this.window as any); 50 const w = (this.window as any);
39 this.mobileChannel = w.flutter_inappwebview; 51 this.mobileChannel = w.flutter_inappwebview;
40 this.mobileApp = isDefined(this.mobileChannel); 52 this.mobileApp = isDefined(this.mobileChannel);
  53 + if (this.mobileApp) {
  54 + window.addEventListener('message', this.onWindowMessageListener);
  55 + }
41 } 56 }
42 57
43 public isMobileApp(): boolean { 58 public isMobileApp(): boolean {
@@ -50,6 +65,12 @@ export class MobileService { @@ -50,6 +65,12 @@ export class MobileService {
50 } 65 }
51 } 66 }
52 67
  68 + public onDashboardLoaded() {
  69 + if (this.mobileApp) {
  70 + this.mobileChannel.callHandler(dashboardLoadedHandler);
  71 + }
  72 + }
  73 +
53 public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]): 74 public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]):
54 Observable<WidgetMobileActionResult<T>> { 75 Observable<WidgetMobileActionResult<T>> {
55 if (this.mobileApp) { 76 if (this.mobileApp) {
@@ -67,4 +88,82 @@ export class MobileService { @@ -67,4 +88,82 @@ export class MobileService {
67 } 88 }
68 } 89 }
69 90
  91 + public handleMobileNavigation(path?: string, params?: Params) {
  92 + if (this.mobileApp) {
  93 + this.mobileChannel.callHandler(navigationHandler, path, params);
  94 + }
  95 + }
  96 +
  97 + private onWindowMessage(event: MessageEvent) {
  98 + if (event.data) {
  99 + let message: WindowMessage;
  100 + try {
  101 + message = JSON.parse(event.data);
  102 + } catch (e) {}
  103 + if (message && message.type) {
  104 + switch (message.type) {
  105 + case 'openDashboardMessage':
  106 + const openDashboardMessage: OpenDashboardMessage = message.data;
  107 + this.openDashboard(openDashboardMessage);
  108 + break;
  109 + case 'reloadUserMessage':
  110 + const reloadUserMessage: ReloadUserMessage = message.data;
  111 + this.reloadUser(reloadUserMessage);
  112 + break;
  113 + }
  114 + }
  115 + }
  116 + }
  117 +
  118 + private openDashboard(openDashboardMessage: OpenDashboardMessage) {
  119 + if (openDashboardMessage && openDashboardMessage.dashboardId) {
  120 + if (this.reloadUserObservable) {
  121 + this.reloadUserObservable.subscribe(
  122 + (authenticated) => {
  123 + if (authenticated) {
  124 + this.doDashboardNavigation(openDashboardMessage);
  125 + }
  126 + }
  127 + );
  128 + } else {
  129 + this.doDashboardNavigation(openDashboardMessage);
  130 + }
  131 + }
  132 + }
  133 +
  134 + private doDashboardNavigation(openDashboardMessage: OpenDashboardMessage) {
  135 + let url = `/dashboard/${openDashboardMessage.dashboardId}`;
  136 + const params = [];
  137 + if (openDashboardMessage.state) {
  138 + params.push(`state=${openDashboardMessage.state}`);
  139 + }
  140 + if (openDashboardMessage.embedded) {
  141 + params.push(`embedded=true`);
  142 + }
  143 + if (openDashboardMessage.hideToolbar) {
  144 + params.push(`hideToolbar=true`);
  145 + }
  146 + if (this.lastDashboardId === openDashboardMessage.dashboardId) {
  147 + params.push(`reload=${new Date().getTime()}`);
  148 + }
  149 + if (params.length) {
  150 + url += `?${params.join('&')}`;
  151 + }
  152 + this.lastDashboardId = openDashboardMessage.dashboardId;
  153 + this.router.navigateByUrl(url, {replaceUrl: true});
  154 + }
  155 +
  156 + private reloadUser(reloadUserMessage: ReloadUserMessage) {
  157 + if (reloadUserMessage && reloadUserMessage.accessToken && reloadUserMessage.refreshToken) {
  158 + this.reloadUserObservable = this.authService.setUserFromJwtToken(reloadUserMessage.accessToken,
  159 + reloadUserMessage.refreshToken, true).pipe(
  160 + tap(
  161 + () => {
  162 + this.reloadUserObservable = null;
  163 + }
  164 + )
  165 + );
  166 + }
  167 + }
  168 +
70 } 169 }
@@ -441,3 +441,7 @@ export function generateSecret(length?: number): string { @@ -441,3 +441,7 @@ export function generateSecret(length?: number): string {
441 export function validateEntityId(entityId: EntityId | null): boolean { 441 export function validateEntityId(entityId: EntityId | null): boolean {
442 return isDefinedAndNotNull(entityId?.id) && entityId.id !== NULL_UUID && isDefinedAndNotNull(entityId?.entityType); 442 return isDefinedAndNotNull(entityId?.id) && entityId.id !== NULL_UUID && isDefinedAndNotNull(entityId?.entityType);
443 } 443 }
  444 +
  445 +export function isMobileApp(): boolean {
  446 + return isDefined((window as any).flutter_inappwebview);
  447 +}
@@ -87,7 +87,7 @@ @@ -87,7 +87,7 @@
87 (click)="updateDashboardImage($event)"> 87 (click)="updateDashboardImage($event)">
88 <mat-icon>wallpaper</mat-icon> 88 <mat-icon>wallpaper</mat-icon>
89 </button> 89 </button>
90 - <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button 90 + <button [fxShow]="currentDashboardId && !isMobileApp && (isEdit || displayExport())" mat-icon-button
91 matTooltip="{{'dashboard.export' | translate}}" 91 matTooltip="{{'dashboard.export' | translate}}"
92 matTooltipPosition="below" 92 matTooltipPosition="below"
93 (click)="exportDashboard($event)"> 93 (click)="exportDashboard($event)">
@@ -323,6 +323,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -323,6 +323,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
323 this.runChangeDetection(); 323 this.runChangeDetection();
324 } 324 }
325 )); 325 ));
  326 + this.rxSubscriptions.push(this.route.queryParamMap.subscribe(
  327 + (paramMap) => {
  328 + if (paramMap.has('reload')) {
  329 + this.dashboardCtx.aliasController.updateAliases();
  330 + setTimeout(() => {
  331 + this.mobileService.handleDashboardStateName(this.dashboardCtx.stateController.getCurrentStateName());
  332 + this.mobileService.onDashboardLoaded();
  333 + });
  334 + }
  335 + }
  336 + ));
326 this.rxSubscriptions.push(this.breakpointObserver 337 this.rxSubscriptions.push(this.breakpointObserver
327 .observe(MediaBreakpoints['gt-sm']) 338 .observe(MediaBreakpoints['gt-sm'])
328 .subscribe((state: BreakpointState) => { 339 .subscribe((state: BreakpointState) => {
@@ -770,6 +781,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -770,6 +781,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
770 this.isRightLayoutOpened = openRightLayout ? true : false; 781 this.isRightLayoutOpened = openRightLayout ? true : false;
771 this.updateLayouts(layoutsData); 782 this.updateLayouts(layoutsData);
772 } 783 }
  784 + setTimeout(() => {
  785 + this.mobileService.onDashboardLoaded();
  786 + });
773 } 787 }
774 788
775 private updateLayouts(layoutsData?: DashboardLayoutsInfo) { 789 private updateLayouts(layoutsData?: DashboardLayoutsInfo) {
@@ -185,6 +185,10 @@ export class DefaultStateControllerComponent extends StateControllerComponent im @@ -185,6 +185,10 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
185 return this.utils.customTranslation(state.name, id); 185 return this.utils.customTranslation(state.name, id);
186 } 186 }
187 187
  188 + public getCurrentStateName(): string {
  189 + return this.getStateName(this.stateObject[0].id, this.statesValue[this.stateObject[0].id]);
  190 + }
  191 +
188 public displayStateSelection(): boolean { 192 public displayStateSelection(): boolean {
189 return this.states && Object.keys(this.states).length > 1; 193 return this.states && Object.keys(this.states).length > 1;
190 } 194 }
@@ -235,6 +235,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp @@ -235,6 +235,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
235 return result; 235 return result;
236 } 236 }
237 237
  238 + public getCurrentStateName(): string {
  239 + return this.getStateName(this.stateObject.length - 1);
  240 + }
  241 +
238 public selectedStateIndexChanged() { 242 public selectedStateIndexChanged() {
239 this.navigatePrevState(this.selectedStateIndex); 243 this.navigatePrevState(this.selectedStateIndex);
240 } 244 }
@@ -198,4 +198,6 @@ export abstract class StateControllerComponent implements IStateControllerCompon @@ -198,4 +198,6 @@ export abstract class StateControllerComponent implements IStateControllerCompon
198 198
199 public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; 199 public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
200 200
  201 + public abstract getCurrentStateName(): string;
  202 +
201 } 203 }
@@ -14,9 +14,21 @@ @@ -14,9 +14,21 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated'; 17 +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated' | 'openDashboardMessage' | 'reloadUserMessage';
18 18
19 export interface WindowMessage { 19 export interface WindowMessage {
20 type: WindowMessageType; 20 type: WindowMessageType;
21 data?: any; 21 data?: any;
22 } 22 }
  23 +
  24 +export interface OpenDashboardMessage {
  25 + dashboardId: string;
  26 + state?: string;
  27 + hideToolbar?: boolean;
  28 + embedded?: boolean;
  29 +}
  30 +
  31 +export interface ReloadUserMessage {
  32 + accessToken: string;
  33 + refreshToken: string;
  34 +}
@@ -174,6 +174,48 @@ $tb-dark-theme: get-tb-dark-theme( @@ -174,6 +174,48 @@ $tb-dark-theme: get-tb-dark-theme(
174 } 174 }
175 } 175 }
176 176
  177 +@mixin _mat-toolbar-inverse-color($palette) {
  178 + background: mat-color($palette, default-contrast);
  179 + color: $dark-primary-text;
  180 +}
  181 +
  182 +@mixin mat-fab-toolbar-inverse-theme($theme) {
  183 + $primary: map-get($theme, primary);
  184 + $accent: map-get($theme, accent);
  185 + $warn: map-get($theme, warn);
  186 + $background: map-get($theme, foreground);
  187 + $foreground: map-get($theme, background);
  188 +
  189 + mat-fab-toolbar {
  190 + .mat-fab-toolbar-background {
  191 + background: mat-color($background, app-bar);
  192 + color: mat-color($foreground, text);
  193 + }
  194 + &.mat-primary {
  195 + .mat-fab-toolbar-background {
  196 + @include _mat-toolbar-inverse-color($primary);
  197 + }
  198 + }
  199 + mat-toolbar {
  200 + &.mat-primary {
  201 + @include _mat-toolbar-inverse-color($primary);
  202 + button.mat-icon-button {
  203 + mat-icon {
  204 + color: mat-color($primary);
  205 + }
  206 + }
  207 + }
  208 + }
  209 + .mat-fab {
  210 + &.mat-primary {
  211 + background: mat-color($primary, default-contrast);
  212 + color: mat-color($primary);
  213 + }
  214 + }
  215 + }
  216 +
  217 +}
  218 +
177 @mixin tb-components-theme($theme) { 219 @mixin tb-components-theme($theme) {
178 $primary: map-get($theme, primary); 220 $primary: map-get($theme, primary);
179 221
@@ -184,6 +226,10 @@ $tb-dark-theme: get-tb-dark-theme( @@ -184,6 +226,10 @@ $tb-dark-theme: get-tb-dark-theme(
184 } 226 }
185 227
186 @include mat-fab-toolbar-theme($tb-theme); 228 @include mat-fab-toolbar-theme($tb-theme);
  229 +
  230 + div.tb-dashboard-page.mobile-app {
  231 + @include mat-fab-toolbar-inverse-theme($tb-theme);
  232 + }
187 } 233 }
188 234
189 .tb-default { 235 .tb-default {
@@ -195,3 +241,4 @@ $tb-dark-theme: get-tb-dark-theme( @@ -195,3 +241,4 @@ $tb-dark-theme: get-tb-dark-theme(
195 .tb-dark { 241 .tb-dark {
196 @include angular-material-theme($tb-dark-theme); 242 @include angular-material-theme($tb-dark-theme);
197 } 243 }
  244 +