Commit bbf78721a7436f91cf761673f3894d88df27895a

Authored by Igor Kulikov
1 parent ed955510

Improve mobile app support

... ... @@ -152,6 +152,7 @@ export interface IStateController {
152 152 getStateIndex(): number;
153 153 getStateIdAtIndex(index: number): string;
154 154 getEntityId(entityParamName: string): EntityId;
  155 + getCurrentStateName(): string;
155 156 }
156 157
157 158 export interface SubscriptionInfo {
... ...
... ... @@ -45,6 +45,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions'
45 45 import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
46 46 import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
47 47 import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
  48 +import { isMobileApp } from '@core/utils';
48 49
49 50 @Injectable({
50 51 providedIn: 'root'
... ... @@ -194,11 +195,13 @@ export class AuthService {
194 195 }
195 196
196 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 207 public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> {
... ... @@ -516,12 +519,15 @@ export class AuthService {
516 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 524 if (!jwtToken) {
521 525 AuthService.clearTokenData();
522 526 if (notify) {
523 527 this.notifyUnauthenticated();
524 528 }
  529 + authenticatedSubject.next(false);
  530 + authenticatedSubject.complete();
525 531 } else {
526 532 this.updateAndValidateTokens(jwtToken, refreshToken, true);
527 533 if (notify) {
... ... @@ -530,16 +536,30 @@ export class AuthService {
530 536 (authPayload) => {
531 537 this.notifyUserLoaded(true);
532 538 this.notifyAuthenticated(authPayload);
  539 + authenticatedSubject.next(true);
  540 + authenticatedSubject.complete();
533 541 },
534 542 () => {
535 543 this.notifyUserLoaded(true);
536 544 this.notifyUnauthenticated();
  545 + authenticatedSubject.next(false);
  546 + authenticatedSubject.complete();
537 547 }
538 548 );
539 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 565 private updateAndValidateTokens(jwtToken, refreshToken, notify: boolean) {
... ...
... ... @@ -29,6 +29,7 @@ import { DialogService } from '@core/services/dialog.service';
29 29 import { TranslateService } from '@ngx-translate/core';
30 30 import { UtilsService } from '@core/services/utils.service';
31 31 import { isObject } from '@core/utils';
  32 +import { MobileService } from '@core/services/mobile.service';
32 33
33 34 @Injectable({
34 35 providedIn: 'root'
... ... @@ -41,6 +42,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
41 42 private dialogService: DialogService,
42 43 private utils: UtilsService,
43 44 private translate: TranslateService,
  45 + private mobileService: MobileService,
44 46 private zone: NgZone) {}
45 47
46 48 getAuthState(): Observable<AuthState> {
... ... @@ -108,6 +110,10 @@ export class AuthGuard implements CanActivate, CanActivateChild {
108 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 117 const defaultUrl = this.authService.defaultUrl(true, authState, path, params);
112 118 if (defaultUrl) {
113 119 // this.authService.gotoDefaultPlace(true);
... ...
... ... @@ -20,9 +20,14 @@ import { isDefined } from '@core/utils';
20 20 import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models';
21 21 import { from, of } from 'rxjs';
22 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 28 const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler';
  29 +const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler';
  30 +const navigationHandler = 'tbMobileNavigationHandler';
26 31 const mobileHandler = 'tbMobileHandler';
27 32
28 33 // @dynamic
... ... @@ -34,10 +39,20 @@ export class MobileService {
34 39 private readonly mobileApp;
35 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 50 const w = (this.window as any);
39 51 this.mobileChannel = w.flutter_inappwebview;
40 52 this.mobileApp = isDefined(this.mobileChannel);
  53 + if (this.mobileApp) {
  54 + window.addEventListener('message', this.onWindowMessageListener);
  55 + }
41 56 }
42 57
43 58 public isMobileApp(): boolean {
... ... @@ -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 74 public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]):
54 75 Observable<WidgetMobileActionResult<T>> {
55 76 if (this.mobileApp) {
... ... @@ -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 441 export function validateEntityId(entityId: EntityId | null): boolean {
442 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 87 (click)="updateDashboardImage($event)">
88 88 <mat-icon>wallpaper</mat-icon>
89 89 </button>
90   - <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
  90 + <button [fxShow]="currentDashboardId && !isMobileApp && (isEdit || displayExport())" mat-icon-button
91 91 matTooltip="{{'dashboard.export' | translate}}"
92 92 matTooltipPosition="below"
93 93 (click)="exportDashboard($event)">
... ...
... ... @@ -323,6 +323,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
323 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 337 this.rxSubscriptions.push(this.breakpointObserver
327 338 .observe(MediaBreakpoints['gt-sm'])
328 339 .subscribe((state: BreakpointState) => {
... ... @@ -770,6 +781,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
770 781 this.isRightLayoutOpened = openRightLayout ? true : false;
771 782 this.updateLayouts(layoutsData);
772 783 }
  784 + setTimeout(() => {
  785 + this.mobileService.onDashboardLoaded();
  786 + });
773 787 }
774 788
775 789 private updateLayouts(layoutsData?: DashboardLayoutsInfo) {
... ...
... ... @@ -185,6 +185,10 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
185 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 192 public displayStateSelection(): boolean {
189 193 return this.states && Object.keys(this.states).length > 1;
190 194 }
... ...
... ... @@ -235,6 +235,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
235 235 return result;
236 236 }
237 237
  238 + public getCurrentStateName(): string {
  239 + return this.getStateName(this.stateObject.length - 1);
  240 + }
  241 +
238 242 public selectedStateIndexChanged() {
239 243 this.navigatePrevState(this.selectedStateIndex);
240 244 }
... ...
... ... @@ -198,4 +198,6 @@ export abstract class StateControllerComponent implements IStateControllerCompon
198 198
199 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 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 19 export interface WindowMessage {
20 20 type: WindowMessageType;
21 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 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 219 @mixin tb-components-theme($theme) {
178 220 $primary: map-get($theme, primary);
179 221
... ... @@ -184,6 +226,10 @@ $tb-dark-theme: get-tb-dark-theme(
184 226 }
185 227
186 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 235 .tb-default {
... ... @@ -195,3 +241,4 @@ $tb-dark-theme: get-tb-dark-theme(
195 241 .tb-dark {
196 242 @include angular-material-theme($tb-dark-theme);
197 243 }
  244 +
... ...