21 changed files with 987 additions and 13 deletions
@ -0,0 +1,21 @@ |
|||||
|
{ |
||||
|
"fqn": "home_page_widgets.mobile_app_qr_code", |
||||
|
"name": "Mobile app QR code", |
||||
|
"deprecated": false, |
||||
|
"image": null, |
||||
|
"description": null, |
||||
|
"descriptor": { |
||||
|
"type": "static", |
||||
|
"sizeX": 6, |
||||
|
"sizeY": 3, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<tb-mobile-app-qrcode-widget\n [ctx]=\"ctx\">\n</tb-mobile-app-qrcode-widget>", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnDestroy();\n}", |
||||
|
"settingsSchema": "", |
||||
|
"dataKeySettingsSchema": "", |
||||
|
"settingsDirective": "", |
||||
|
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" |
||||
|
}, |
||||
|
"tags": null |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { MobileAppSettings } from '@shared/models/mobile-app.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class MobileAppService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public getMobileAppSettings(config?: RequestConfig): Observable<MobileAppSettings> { |
||||
|
return this.http.get<MobileAppSettings>(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable<MobileAppSettings> { |
||||
|
return this.http.post<MobileAppSettings>(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getMobileAppDeepLink( config?: RequestConfig): Observable<string> { |
||||
|
return this.http.get<string>(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div *ngIf="!previewMode" class="tb-title" translate>admin.mobile-app.connect-mobile-app</div> |
||||
|
<div class="tb-flex column center align-center"> |
||||
|
<div class="tb-flex row align-center shrink" style="margin-bottom: 8px" |
||||
|
[class.row]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.RIGHT" |
||||
|
[class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT"> |
||||
|
<canvas #canvas class="tb-qrcode"></canvas> |
||||
|
<div *ngIf="mobileAppSettings?.qrCodeConfig.badgeEnabled" |
||||
|
class="tb-flex column no-gap tb-badge-container" |
||||
|
[class.tb-badge-container]="!previewMode"> |
||||
|
<ng-container *ngIf="mobileAppSettings.qrCodeConfig.badgeStyle === badgeStyle.ORIGINAL;else white"> |
||||
|
<img *ngIf="mobileAppSettings.iosConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.ORIGINAL).iOS"> |
||||
|
<img *ngIf="mobileAppSettings.androidConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.ORIGINAL).android"> |
||||
|
</ng-container> |
||||
|
<ng-template #white> |
||||
|
<img *ngIf="mobileAppSettings.iosConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.WHITE).iOS"> |
||||
|
<img *ngIf="mobileAppSettings.androidConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.WHITE).android"> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="mobileAppSettings?.qrCodeConfig.qrCodeLabelEnabled" class="tb-qrcode-label"> |
||||
|
{{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
@import '../../../../../../scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
|
||||
|
.tb-title { |
||||
|
align-self: start; |
||||
|
font-weight: 600; |
||||
|
font-size: 20px; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.1px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.25px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-qrcode-label { |
||||
|
font-size: 14px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
|
||||
|
.tb-qrcode { |
||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08); |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
|
.tb-badge-container { |
||||
|
@media #{$mat-lt-lg} { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; |
||||
|
import { MobileAppService } from '@core/http/mobile-app.service'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; |
||||
|
import { MINUTE } from '@shared/models/time/time.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mobile-app-qrcode-widget', |
||||
|
templateUrl: './mobile-app-qrcode-widget.component.html', |
||||
|
styleUrls: ['./mobile-app-qrcode-widget.component.scss'] |
||||
|
}) |
||||
|
export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
previewMode: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
set mobileAppSettings(settings: MobileAppSettings) { |
||||
|
if (settings) { |
||||
|
this.mobileAppSettingsValue = settings; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
get mobileAppSettings() { |
||||
|
return this.mobileAppSettingsValue; |
||||
|
} |
||||
|
|
||||
|
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>; |
||||
|
|
||||
|
private readonly destroy$ = new Subject<void>(); |
||||
|
|
||||
|
badgeStyle = BadgeStyle; |
||||
|
badgePosition = BadgePosition; |
||||
|
badgeStyleURLMap = badgeStyleURLMap; |
||||
|
|
||||
|
private mobileAppSettingsValue: MobileAppSettings; |
||||
|
private deepLinkTTL: number; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected cd: ChangeDetectorRef, |
||||
|
private mobileAppService: MobileAppService, |
||||
|
private utilsService: UtilsService) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
if (this.ctx) { |
||||
|
this.ctx.$scope.mobileAppQrcodeWidget = this; |
||||
|
} |
||||
|
if (!this.mobileAppSettings) { |
||||
|
this.mobileAppService.getMobileAppSettings().subscribe((settings => { |
||||
|
this.mobileAppSettings = settings; |
||||
|
this.cd.detectChanges(); |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
this.getMobileAppDeepLink().subscribe(link => { |
||||
|
this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; |
||||
|
this.updateQRCode(link); |
||||
|
interval(this.deepLinkTTL).pipe( |
||||
|
takeUntil(this.destroy$), |
||||
|
mergeMap(() => this.getMobileAppDeepLink()) |
||||
|
).subscribe(link => this.updateQRCode(link)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
super.ngOnDestroy(); |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
getMobileAppDeepLink(): Observable<string> { |
||||
|
return this.mobileAppService.getMobileAppDeepLink(); |
||||
|
} |
||||
|
|
||||
|
updateQRCode(link: string) { |
||||
|
import('qrcode').then((QRCode) => { |
||||
|
QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 125}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-card appearance="outlined" class="settings-card"> |
||||
|
<mat-card-header> |
||||
|
<mat-card-title> |
||||
|
<span class="mat-headline-5" translate>admin.mobile-app.mobile-app-qr-code-widget-settings</span> |
||||
|
</mat-card-title> |
||||
|
</mat-card-header> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<mat-card-content style="padding-top: 16px"> |
||||
|
<div class="tb-form-panel no-border no-padding" [formGroup]="mobileAppSettingsForm"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div fxLayout="row" fxLayoutAlign="space-between center"> |
||||
|
<div class="tb-form-panel-title" translate>admin.mobile-app.applications</div> |
||||
|
<tb-toggle-select formControlName="useDefaultApp"> |
||||
|
<tb-toggle-option [value]="true">{{ 'admin.mobile-app.default' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="false">{{ 'admin.mobile-app.custom' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked no-padding no-gap" formGroupName="androidConfig"> |
||||
|
<div class="tb-form-row no-border no-padding-bottom"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.android' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding-top" |
||||
|
*ngIf="!mobileAppSettingsForm.get('useDefaultApp').value && mobileAppSettingsForm.get('androidConfig.enabled').value"> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<div class="tb-fixed-width">{{ 'admin.mobile-app.app-package-name' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="appPackage" placeholder="{{'admin.mobile-app.set' | translate}}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'admin.mobile-app.app-package-name-required' | translate" |
||||
|
*ngIf="mobileAppSettingsForm.get('androidConfig.appPackage').hasError('required') |
||||
|
&& mobileAppSettingsForm.get('androidConfig.appPackage').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding-top" |
||||
|
*ngIf="!mobileAppSettingsForm.get('useDefaultApp').value && mobileAppSettingsForm.get('androidConfig.enabled').value"> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<div class="tb-fixed-width">{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="sha256CertFingerprints" placeholder="{{'admin.mobile-app.set' | translate}}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'admin.mobile-app.sha256-certificate-fingerprints-required' | translate" |
||||
|
*ngIf="mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').hasError('required') |
||||
|
&& mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked no-padding no-gap" formGroupName="iosConfig"> |
||||
|
<div class="tb-form-row no-border no-padding-bottom"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.ios' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding-top" |
||||
|
*ngIf="!mobileAppSettingsForm.get('useDefaultApp').value && mobileAppSettingsForm.get('iosConfig.enabled').value"> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<div class="tb-fixed-width">{{ 'admin.mobile-app.app-id' | translate }}</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="appId" placeholder="{{'admin.mobile-app.set' | translate}}"> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'admin.mobile-app.app-id-required' | translate" |
||||
|
*ngIf="mobileAppSettingsForm.get('iosConfig.appId').hasError('required') |
||||
|
&& mobileAppSettingsForm.get('iosConfig.appId').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel" formGroupName="qrCodeConfig"> |
||||
|
<div class="tb-flex row space-between align-center"> |
||||
|
<div class="tb-form-panel-title" translate>admin.mobile-app.appearance-on-home-page</div> |
||||
|
<tb-toggle-select formControlName="showOnHomePage"> |
||||
|
<tb-toggle-option [value]="true">{{ 'admin.mobile-app.enabled' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="false">{{ 'admin.mobile-app.disabled' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked no-padding no-gap" *ngIf="mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value"> |
||||
|
<div class="tb-form-row space-between no-border no-padding-bottom column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide tb-fixed-width" formControlName="badgeEnabled" |
||||
|
(click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.badges' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="badgeStyle" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-option *ngFor="let badgeStyle of badgeStyleTranslationsMap | keyvalue" [value]="badgeStyle.key"> |
||||
|
{{ badgeStyle.value | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="badgePosition" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-option *ngFor="let badgePosition of badgePositionTranslationsMap | keyvalue" [value]="badgePosition.key"> |
||||
|
{{ badgePosition.value | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked no-padding no-gap" *ngIf="mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value"> |
||||
|
<div class="tb-form-row space-between no-border no-padding-bottom column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide tb-fixed-width" formControlName="qrCodeLabelEnabled" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.label' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="qrCodeLabel" maxlength="50" placeholder="{{'admin.mobile-app.set' | translate}}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-qrcode-preview" *ngIf="mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value"> |
||||
|
<div class="tb-form-panel-title" translate>admin.mobile-app.preview</div> |
||||
|
<tb-mobile-app-qrcode-widget previewMode |
||||
|
[mobileAppSettings]="mobileAppSettingsForm.getRawValue()"> |
||||
|
</tb-mobile-app-qrcode-widget> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-flex row flex-end" style="margin-top: 16px"> |
||||
|
<button mat-button mat-raised-button color="primary" (click)="save()" |
||||
|
[disabled]="(isLoading$ | async) || mobileAppSettingsForm.invalid || !mobileAppSettingsForm.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
.tb-fixed-width { |
||||
|
min-width: 230px; |
||||
|
} |
||||
|
|
||||
|
.tb-qrcode-preview { |
||||
|
background-color: rgba(0, 0, 0, 0.04); |
||||
|
box-shadow: none; |
||||
|
|
||||
|
.tb-form-panel-title { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,197 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnDestroy } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
import { Subject, takeUntil } from 'rxjs'; |
||||
|
import { MobileAppService } from '@core/http/mobile-app.service'; |
||||
|
import { |
||||
|
BadgePosition, |
||||
|
badgePositionTranslationsMap, |
||||
|
BadgeStyle, |
||||
|
badgeStyleTranslationsMap, |
||||
|
MobileAppSettings |
||||
|
} from '@shared/models/mobile-app.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mobile-app-settings', |
||||
|
templateUrl: './mobile-app-settings.component.html', |
||||
|
styleUrls: ['mobile-app-settings.component.scss', './settings-card.scss'] |
||||
|
}) |
||||
|
export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { |
||||
|
|
||||
|
mobileAppSettingsForm: FormGroup; |
||||
|
|
||||
|
mobileAppSettings: MobileAppSettings; |
||||
|
|
||||
|
private readonly destroy$ = new Subject<void>(); |
||||
|
|
||||
|
badgePositionTranslationsMap = badgePositionTranslationsMap; |
||||
|
badgeStyleTranslationsMap = badgeStyleTranslationsMap; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private mobileAppService: MobileAppService, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
this.buildMobileAppSettingsForm(); |
||||
|
this.mobileAppService.getMobileAppSettings() |
||||
|
.subscribe(settings => this.processMobileAppSettings(settings)); |
||||
|
this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); |
||||
|
} |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); |
||||
|
} |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); |
||||
|
} |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); |
||||
|
} |
||||
|
} else { |
||||
|
if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); |
||||
|
} |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); |
||||
|
} |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); |
||||
|
} |
||||
|
} else { |
||||
|
if (!this.mobileAppSettingsForm.get('androidConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (value) { |
||||
|
if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); |
||||
|
} |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); |
||||
|
} |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
super.ngOnDestroy(); |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
private buildMobileAppSettingsForm() { |
||||
|
this.mobileAppSettingsForm = this.fb.group({ |
||||
|
useDefaultApp: [true, []], |
||||
|
androidConfig: this.fb.group({ |
||||
|
enabled: [true, []], |
||||
|
appPackage: ['', []], |
||||
|
sha256CertFingerprints: ['', []] |
||||
|
}), |
||||
|
iosConfig: this.fb.group({ |
||||
|
enabled: [true, []], |
||||
|
appId: ['', []] |
||||
|
}), |
||||
|
qrCodeConfig: this.fb.group({ |
||||
|
showOnHomePage: [true, []], |
||||
|
badgeEnabled: [true, []], |
||||
|
badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}, []], |
||||
|
badgePosition: [{value: BadgePosition.RIGHT, disabled: true}, []], |
||||
|
qrCodeLabelEnabled: [true, []], |
||||
|
qrCodeLabel: ['', []] |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { |
||||
|
this.mobileAppSettings = {...mobileAppSettings}; |
||||
|
this.mobileAppSettingsForm.reset(this.mobileAppSettings); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; |
||||
|
this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) |
||||
|
.subscribe((settings) => this.processMobileAppSettings(settings)); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.mobileAppSettingsForm; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { TenantId } from '@shared/models/id/tenant-id'; |
||||
|
|
||||
|
export interface MobileAppSettings { |
||||
|
tenantId: TenantId; |
||||
|
useDefaultApp: boolean; |
||||
|
androidConfig: AndroidConfig; |
||||
|
iosConfig: IosConfig; |
||||
|
qrCodeConfig: QRCodeConfig; |
||||
|
} |
||||
|
|
||||
|
export interface AndroidConfig { |
||||
|
enabled: boolean; |
||||
|
appPackage: string; |
||||
|
sha256CertFingerprints: string |
||||
|
} |
||||
|
|
||||
|
export interface IosConfig { |
||||
|
enabled: boolean; |
||||
|
appId: string; |
||||
|
} |
||||
|
|
||||
|
export interface QRCodeConfig { |
||||
|
showOnHomePage: boolean; |
||||
|
badgeEnabled: boolean; |
||||
|
badgePosition: BadgePosition; |
||||
|
badgeStyle: BadgeStyle; |
||||
|
qrCodeLabelEnabled: boolean; |
||||
|
qrCodeLabel: string; |
||||
|
} |
||||
|
|
||||
|
export interface MobileOSBadgeURL { |
||||
|
iOS: string; |
||||
|
android: string; |
||||
|
} |
||||
|
|
||||
|
export enum BadgePosition { |
||||
|
RIGHT = 'RIGHT', |
||||
|
LEFT = 'LEFT' |
||||
|
} |
||||
|
|
||||
|
export const badgePositionTranslationsMap = new Map<BadgePosition, string>([ |
||||
|
[BadgePosition.RIGHT, 'admin.mobile-app.right'], |
||||
|
[BadgePosition.LEFT, 'admin.mobile-app.left'] |
||||
|
]); |
||||
|
|
||||
|
export enum BadgeStyle { |
||||
|
ORIGINAL = 'ORIGINAL', |
||||
|
WHITE = 'WHITE' |
||||
|
} |
||||
|
|
||||
|
export const badgeStyleTranslationsMap = new Map<BadgeStyle, string>([ |
||||
|
[BadgeStyle.ORIGINAL, 'admin.mobile-app.original'], |
||||
|
[BadgeStyle.WHITE, 'admin.mobile-app.white'] |
||||
|
]); |
||||
|
|
||||
|
export const badgeStyleURLMap = new Map<BadgeStyle, MobileOSBadgeURL>([ |
||||
|
[BadgeStyle.ORIGINAL, { |
||||
|
iOS: 'assets/android-ios-stores-badges/ios_store_en_black_badge.svg', |
||||
|
android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' |
||||
|
}], |
||||
|
[BadgeStyle.WHITE, { |
||||
|
iOS: 'assets/android-ios-stores-badges/ios_store_en_white_badge.svg', |
||||
|
android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' |
||||
|
}] |
||||
|
]); |
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
Loading…
Reference in new issue