Browse Source

Merge branch 'qrCode' of github.com:dashevchenko/thingsboard into qrCode

pull/10591/head
Vladyslav_Prykhodko 2 years ago
parent
commit
034ed6219b
  1. 1
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 3
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  3. 21
      application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json
  4. 21
      application/src/main/data/json/system/widget_types/mobile_app_qr_code.json
  5. 15
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  6. 2
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  7. 188
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  8. 10
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  9. 28
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java
  10. 66
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
  11. 33
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java
  12. 25
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java
  13. 36
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  16. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  17. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  18. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  19. 6
      application/src/main/resources/thingsboard.yml
  20. 253
      application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
  21. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  22. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  23. 37
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java
  24. 38
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
  25. 23
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java
  26. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java
  27. 36
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
  28. 45
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
  29. 40
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java
  30. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  32. 112
      dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
  33. 34
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java
  34. 28
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java
  35. 24
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java
  36. 36
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java
  37. 29
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java
  38. 9
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  39. 85
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java
  40. 51
      dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java
  41. 61
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java
  42. 36
      dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java
  43. 6
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  44. 13
      dao/src/main/resources/sql/schema-entities.sql
  45. 45
      ui-ngx/src/app/core/http/mobile-app.service.ts
  46. 8
      ui-ngx/src/app/core/services/menu.service.ts
  47. 5
      ui-ngx/src/app/core/services/utils.service.ts
  48. 50
      ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts
  49. 41
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html
  50. 57
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss
  51. 114
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts
  52. 130
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html
  53. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss
  54. 156
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts
  55. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  56. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  57. 14
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  58. 18
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  59. 161
      ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html
  60. 28
      ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss
  61. 198
      ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts
  62. 81
      ui-ngx/src/app/shared/models/mobile-app.models.ts
  63. 35
      ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg
  64. 46
      ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg
  65. 46
      ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg
  66. 53
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  67. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  68. 12
      ui-ngx/src/form.scss

1
application/src/main/data/json/system/widget_bundles/cards.json

@ -17,6 +17,7 @@
"cards.label_widget",
"cards.dashboard_state_widget",
"cards.qr_code",
"cards.mobile_app_qr_code",
"cards.attributes_card",
"cards.html_card",
"cards.html_value_card",

3
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -13,6 +13,7 @@
"home_page_widgets.quick_links",
"home_page_widgets.documentation_links",
"home_page_widgets.dashboards",
"home_page_widgets.usage_info"
"home_page_widgets.usage_info",
"home_page_widgets.home_mobile_app_qr_code"
]
}

21
application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json

@ -0,0 +1,21 @@
{
"fqn": "home_page_widgets.home_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</tb-mobile-app-qrcode-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\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\":\"Mobile app QR code\",\"dropShadow\":true}"
},
"tags": null
}

21
application/src/main/data/json/system/widget_types/mobile_app_qr_code.json

@ -0,0 +1,21 @@
{
"fqn": "cards.mobile_app_qr_code",
"name": "Mobile app QR code",
"deprecated": false,
"image": null,
"description": null,
"descriptor": {
"type": "static",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-mobile-app-qrcode-widget\n [ctx]=\"ctx\">\n</tb-mobile-app-qrcode-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-mobile-app-qr-code-widget-settings",
"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\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"useDefaultApp\":true,\"androidConfig\":{\"enabled\":true,\"appPackage\":\"\",\"sha256CertFingerprints\":\"\"},\"iosConfig\":{\"enabled\":true,\"appId\":\"\"},\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgeStyle\":\"ORIGINAL\",\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
},
"tags": null
}

15
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -145,3 +145,18 @@ DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue';
-- QUEUE STATS UPDATE END
-- MOBILE APP SETTINGS TABLE CREATE START
CREATE TABLE IF NOT EXISTS mobile_app_settings (
id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
use_default_app boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
qr_code_config VARCHAR(100000),
CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
);
-- MOBILE APP SETTINGS TABLE CREATE END

2
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -76,7 +76,7 @@ public class ThingsboardSecurityConfiguration {
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"};
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";

188
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java

@ -0,0 +1,188 @@
/**
* 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.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.mobile.secret.MobileAppSecretService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
@RequiredArgsConstructor
@RestController
@TbCoreComponent
public class MobileApplicationController extends BaseController {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
public static final String ASSET_LINKS_PATTERN = "[{\n" +
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
" \"target\": {\n" +
" \"namespace\": \"android_app\",\n" +
" \"package_name\": \"%s\",\n" +
" \"sha256_cert_fingerprints\":\n" +
" [\"%s\"]\n" +
" }\n" +
"}]";
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" +
" \"applinks\": {\n" +
" \"apps\": [],\n" +
" \"details\": [\n" +
" {\n" +
" \"appID\": \"%s\",\n" +
" \"paths\": [ \"/api/noauth/qr\" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app";
public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695";
public static final String SECRET = "secret";
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key";
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s";
private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService;
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)")
@GetMapping(value = "/.well-known/assetlinks.json")
public ResponseEntity<JsonNode> getAssetLinks() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
@GetMapping(value = "/.well-known/apple-app-site-association")
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
IosConfig iosConfig = mobileAppSettings.getIosConfig();
if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping(value = "/api/mobile/app/settings")
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
mobileAppSettings.setTenantId(getTenantId());
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/app/settings")
public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
}
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)",
notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain")
public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException {
String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser());
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request);
String platformDomain = new URI(baseUrl).getHost();
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
String appDomain;
if (!mobileAppSettings.isUseDefaultApp()) {
appDomain = platformDomain;
} else {
appDomain = DEFAULT_APP_DOMAIN;
}
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
if (!appDomain.equals(platformDomain)) {
deepLink = deepLink + "&host=" + baseUrl;
}
return "\"" + deepLink + "\"";
}
@ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)",
notes = "Returns the token of the User based on the provided secret key.")
@GetMapping(value = "/api/noauth/qr/{secret}")
public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION)
@PathVariable(SECRET) String secret) throws ThingsboardException {
checkParameter(SECRET, secret);
return mobileAppSecretService.getJwtPair(secret);
}
@GetMapping(value = "/api/noauth/qr")
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
if (userAgent.contains("Android")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", ANDROID_APPLICATION_STORE_LINK)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", APPLE_APPLICATION_STORE_LINK)
.build();
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.build();
}
}
}

10
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -34,10 +34,13 @@ import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -45,6 +48,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Hidden
@ -72,6 +76,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private EntitiesVersionControlService versionControlService;
@Autowired
private MobileAppSettingsService mobileAppSettingsService;
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@ -135,6 +142,9 @@ public class SystemInfoController extends BaseController {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
}
systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID))
.map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
.orElse(false));
return systemParams;
}

28
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java

@ -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.
*/
package org.thingsboard.server.service.mobile.secret;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface MobileAppSecretService {
String generateMobileAppSecret(SecurityUser securityUser);
JwtPair getJwtPair(String secret) throws ThingsboardException;
}

66
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java

@ -0,0 +1,66 @@
/**
* 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.
*/
package org.thingsboard.server.service.mobile.secret;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.entity.AbstractCachedService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
@Service
@Slf4j
@RequiredArgsConstructor
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
@Override
public String generateMobileAppSecret(SecurityUser securityUser) {
log.trace("Executing generateSecret for user [{}]", securityUser.getId());
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
cache.put(secret, tokenFactory.createTokenPair(securityUser));
return secret;
}
@Override
public JwtPair getJwtPair(String secret) throws ThingsboardException {
TbCacheValueWrapper<JwtPair> jwtPair = cache.get(secret);
if (jwtPair != null) {
return jwtPair.get();
} else {
throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED);
}
}
@TransactionalEventListener(classes = MobileSecretEvictEvent.class)
@Override
public void handleEvictEvent(MobileSecretEvictEvent event) {
cache.evict(event.getSecret());
}
}

33
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.
*/
package org.thingsboard.server.service.mobile.secret;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("MobileSecretCache")
public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> {
public MobileSecretCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE);
}
}

25
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java

@ -0,0 +1,25 @@
/**
* 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.
*/
package org.thingsboard.server.service.mobile.secret;
import lombok.Data;
@Data
public class MobileSecretEvictEvent {
private final String secret;
}

36
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java

@ -0,0 +1,36 @@
/**
* 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.
*/
package org.thingsboard.server.service.mobile.secret;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("MobileSecretCache")
public class MobileSecretRedisCache extends RedisTbTransactionalCache<UserId, JwtPair> {
public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
}
}

1
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java

@ -49,6 +49,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker);
put(Resource.TB_RESOURCE, customerResourcePermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {

4
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -46,8 +46,8 @@ public enum Resource {
QUEUE(EntityType.QUEUE),
VERSION_CONTROL,
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE);
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS;
private final Set<EntityType> entityTypes;
Resource() {

1
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -41,6 +41,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION, systemEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker);
}
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -50,6 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.QUEUE, queuePermissionChecker);
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

4
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -57,7 +57,6 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client;
@ -75,6 +74,8 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
@Slf4j
public class DefaultSystemSecurityService implements SystemSecurityService {
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
@Autowired
private AdminSettingsService adminSettingsService;
@ -109,6 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
}
return securitySettings;
}

6
application/src/main/resources/thingsboard.yml

@ -585,6 +585,12 @@ cache:
alarmTypes:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL
maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileAppSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:

253
application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java

@ -0,0 +1,253 @@
/**
* 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.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class MobileApplicationControllerTest extends AbstractControllerTest {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
private static final String ANDROID_PACKAGE_NAME = "testAppPackage";
private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC";
private static final String APPLE_APP_ID = "testId";
private static final String TEST_LABEL = "Test label";
@Before
public void setUp() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
mobileAppSettings.setUseDefaultApp(true);
AndroidConfig androidConfig = AndroidConfig.builder()
.appPackage(ANDROID_PACKAGE_NAME)
.sha256CertFingerprints(ANDROID_APP_SHA256)
.enabled(true)
.build();
IosConfig iosConfig = IosConfig.builder()
.appId(APPLE_APP_ID)
.enabled(true)
.build();
mobileAppSettings.setAndroidConfig(androidConfig);
mobileAppSettings.setIosConfig(iosConfig);
mobileAppSettings.setQrCodeConfig(qrCodeConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testSaveMobileAppSettings() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL);
assertThat(mobileAppSettings.isUseDefaultApp()).isTrue();
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse();
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
mobileAppSettings.setAndroidConfig(null);
mobileAppSettings.setIosConfig(null);
mobileAppSettings.setQrCodeConfig(null);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Qr code configuration is required!")));
mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
AndroidConfig androidConfig = AndroidConfig.builder()
.enabled(true)
.appPackage(null)
.sha256CertFingerprints(null)
.build();
mobileAppSettings.setAndroidConfig(androidConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
androidConfig.setAppPackage("test_app_package");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
androidConfig.setSha256CertFingerprints("test_sha_256");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
IosConfig iosConfig = IosConfig.builder()
.enabled(true)
.appId(null)
.build();
mobileAppSettings.setIosConfig(iosConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application id is required for custom ios application!")));
iosConfig.setAppId("test_app_id");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(true);
mobileAppSettings.setIosConfig(null);
mobileAppSettings.setAndroidConfig(null);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testGetApplicationAssociations() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class);
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME);
assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256);
JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class);
assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID);
}
@Test
public void testGetMobileDeepLink() throws Exception {
loginSysAdmin();
String deepLink = doGet("/api/mobile/deepLink", String.class);
Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\"");
Matcher parsedDeepLink = expectedPattern.matcher(deepLink);
assertThat(parsedDeepLink.matches()).isTrue();
String appHost = parsedDeepLink.group(1);
String secret = parsedDeepLink.group(2);
String ttl = parsedDeepLink.group(3);
assertThat(appHost).isEqualTo("demo.thingsboard.io");
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl));
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class);
assertThat(jwtPair).isNotNull();
loginTenantAdmin();
String tenantDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink);
assertThat(tenantParsedDeepLink.matches()).isTrue();
String tenantSecret = tenantParsedDeepLink.group(2);
JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class);
assertThat(tenantJwtPair).isNotNull();
loginCustomerUser();
String customerDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink);
assertThat(customerParsedDeepLink.matches()).isTrue();
String customerSecret = customerParsedDeepLink.group(2);
JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class);
assertThat(customerJwtPair).isNotNull();
// update mobile setting to use custom one
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings);
String customAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\"");
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink);
assertThat(customAppParsedDeepLink.matches()).isTrue();
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginTenantAdmin();
String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginCustomerUser();
String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -48,4 +48,6 @@ public class CacheConstants {
public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
public static final String ALARM_TYPES_CACHE = "alarmTypes";
public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings";
public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
}

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -31,4 +31,5 @@ public class SystemParams {
JsonNode userSettings;
long maxDatapointsLimit;
long maxResourceSize;
boolean mobileQrEnabled;
}

37
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java

@ -0,0 +1,37 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
@Schema
public class MobileAppSettingsId extends UUIDBased {
private static final long serialVersionUID = 1L;
@JsonCreator
public MobileAppSettingsId(@JsonProperty("id") UUID id) {
super(id);
}
public static MobileAppSettingsId fromString(String mobileAppSettingsId) {
return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId));
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java

@ -0,0 +1,38 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class AndroidConfig {
private boolean enabled;
@NoXss
private String appPackage;
@NoXss
private String sha256CertFingerprints;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java

@ -0,0 +1,23 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.mobile;
public enum BadgePosition {
RIGHT,
LEFT;
}

24
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java

@ -0,0 +1,24 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.mobile;
public enum BadgeStyle {
ORIGINAL,
WHITE;
}

36
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java

@ -0,0 +1,36 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class IosConfig {
private boolean enabled;
@NoXss
private String appId;
}

45
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java

@ -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.
*/
package org.thingsboard.server.common.data.mobile;
import jakarta.validation.Valid;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.MobileAppSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId {
private static final long serialVersionUID = 2628323657987010348L;
private TenantId tenantId;
private boolean useDefaultApp;
@Valid
private AndroidConfig androidConfig;
@Valid
private IosConfig iosConfig;
@Valid
private QRCodeConfig qrCodeConfig;
public MobileAppSettings() {
}
public MobileAppSettings(MobileAppSettingsId id) {
super(id);
}
}

40
common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java

@ -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.
*/
package org.thingsboard.server.common.data.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class QRCodeConfig {
private boolean showOnHomePage;
private boolean badgeEnabled;
private boolean qrCodeLabelEnabled;
private BadgePosition badgePosition;
private BadgeStyle badgeStyle;
@NoXss
private String qrCodeLabel;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java

@ -20,10 +20,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority;
import java.io.Serializable;
@Schema(description = "JWT Pair")
@Data
@NoArgsConstructor
public class JwtPair {
public class JwtPair implements Serializable {
@Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token;

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java

@ -32,4 +32,6 @@ public class SecuritySettings implements Serializable {
private Integer maxFailedLoginAttempts;
@Schema(description = "Email to use for notifications about locked users." )
private String userLockoutNotificationEmail;
@Schema(description = "Mobile secret key length" )
private Integer mobileSecretKeyLength;
}

112
dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java

@ -0,0 +1,112 @@
/**
* 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.
*/
package org.thingsboard.server.dao.mobile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.BadgePosition;
import org.thingsboard.server.common.data.mobile.BadgeStyle;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Map;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
@RequiredArgsConstructor
public class BaseMobileAppSettingsService extends AbstractCachedEntityService<TenantId, MobileAppSettings, MobileAppSettingsEvictEvent> implements MobileAppSettingsService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app";
private final MobileAppSettingsDao mobileAppSettingsDao;
private final DataValidator<MobileAppSettings> mobileAppSettingsDataValidator;
@Override
public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings mobileAppSettings) {
mobileAppSettingsDataValidator.validate(mobileAppSettings, s -> tenantId);
try {
MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings);
publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
return savedMobileAppSettings;
} catch (Exception e) {
handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
checkConstraintViolation(e, Map.of(
"mobile_app_settings_tenant_id_unq_key", "Mobile application for specified tenant already exists!"
));
throw e;
}
}
@Override
public MobileAppSettings getMobileAppSettings(TenantId tenantId) {
log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId);
MobileAppSettings mobileAppSettings = cache.getAndPutInTransaction(tenantId,
() -> mobileAppSettingsDao.findByTenantId(tenantId), true);
return constructMobileAppSettings(mobileAppSettings);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
mobileAppSettingsDao.removeByTenantId(tenantId);
}
@TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class)
@Override
public void handleEvictEvent(MobileAppSettingsEvictEvent event) {
cache.evict(event.getTenantId());
}
private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) {
if (mobileAppSettings == null) {
mobileAppSettings = new MobileAppSettings();
mobileAppSettings.setUseDefaultApp(true);
AndroidConfig androidConfig = AndroidConfig.builder()
.enabled(true)
.build();
IosConfig iosConfig = IosConfig.builder()
.enabled(true)
.build();
QRCodeConfig qrCodeConfig = QRCodeConfig.builder()
.showOnHomePage(true)
.qrCodeLabelEnabled(true)
.qrCodeLabel(DEFAULT_QR_CODE_LABEL)
.badgeEnabled(true)
.badgePosition(BadgePosition.RIGHT)
.badgeStyle(BadgeStyle.ORIGINAL)
.badgeEnabled(true)
.build();
mobileAppSettings.setQrCodeConfig(qrCodeConfig);
mobileAppSettings.setAndroidConfig(androidConfig);
mobileAppSettings.setIosConfig(iosConfig);
}
return mobileAppSettings;
}
}

34
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java

@ -0,0 +1,34 @@
/**
* 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.
*/
package org.thingsboard.server.dao.mobile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("MobileAppCache")
public class MobileAppSettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, MobileAppSettings> {
public MobileAppSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE);
}
}

28
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java

@ -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.
*/
package org.thingsboard.server.dao.mobile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.dao.Dao;
public interface MobileAppSettingsDao extends Dao<MobileAppSettings> {
MobileAppSettings findByTenantId(TenantId tenantId);
void removeByTenantId(TenantId tenantId);
}

24
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java

@ -0,0 +1,24 @@
/**
* 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.
*/
package org.thingsboard.server.dao.mobile;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class MobileAppSettingsEvictEvent {
private final TenantId tenantId;
}

36
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java

@ -0,0 +1,36 @@
/**
* 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.
*/
package org.thingsboard.server.dao.mobile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("MobileAppCache")
public class MobileAppSettingsRedisCache extends RedisTbTransactionalCache<TenantId, MobileAppSettings> {
public MobileAppSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.MOBILE_APP_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(MobileAppSettings.class));
}
}

29
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java

@ -0,0 +1,29 @@
/**
* 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.
*/
package org.thingsboard.server.dao.mobile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
public interface MobileAppSettingsService {
MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings);
MobileAppSettings getMobileAppSettings(TenantId tenantId);
void deleteByTenantId(TenantId tenantId);
}

9
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -660,6 +660,15 @@ public class ModelConstants {
public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type";
public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration";
/**
* Mobile application settings constants.
*/
public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings";
public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app";
public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config";
public static final String MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config";
public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};

85
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java

@ -0,0 +1,85 @@
/**
* 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.
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.MobileAppSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Entity
@Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME)
public class MobileAppSettingsEntity extends BaseSqlEntity<MobileAppSettings> {
@Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid")
protected UUID tenantId;
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY)
private boolean useDefaultApp;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY)
private JsonNode androidConfig;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY)
private JsonNode iosConfig;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY)
private JsonNode qrCodeConfig;
public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) {
this.setId(mobileAppSettings.getUuidId());
this.setCreatedTime(mobileAppSettings.getCreatedTime());
this.tenantId = mobileAppSettings.getTenantId().getId();
this.useDefaultApp = mobileAppSettings.isUseDefaultApp();
this.androidConfig = toJson(mobileAppSettings.getAndroidConfig());
this.iosConfig = toJson(mobileAppSettings.getIosConfig());
this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig());
}
@Override
public MobileAppSettings toData() {
MobileAppSettings mobileAppSettings = new MobileAppSettings(new MobileAppSettingsId(getUuid()));
mobileAppSettings.setCreatedTime(createdTime);
mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId));
mobileAppSettings.setUseDefaultApp(useDefaultApp);
mobileAppSettings.setAndroidConfig(fromJson(androidConfig, AndroidConfig.class));
mobileAppSettings.setIosConfig(fromJson(iosConfig, IosConfig.class));
mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class));
return mobileAppSettings;
}
}

51
dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java

@ -0,0 +1,51 @@
/**
* 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.
*/
package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@Component
@AllArgsConstructor
public class MobileAppSettingsDataValidator extends DataValidator<MobileAppSettings> {
@Override
protected void validateDataImpl(TenantId tenantId, MobileAppSettings mobileAppSettings) {
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
IosConfig iosConfig = mobileAppSettings.getIosConfig();
QRCodeConfig qrCodeConfig = mobileAppSettings.getQrCodeConfig();
if (!mobileAppSettings.isUseDefaultApp() && (androidConfig == null || iosConfig == null)) {
throw new DataValidationException("Android/ios settings are required to use custom application!");
}
if (qrCodeConfig == null) {
throw new DataValidationException("Qr code configuration is required!");
}
if (androidConfig != null && androidConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() &&
(androidConfig.getAppPackage() == null || androidConfig.getSha256CertFingerprints() == null)) {
throw new DataValidationException("Application package and sha256 cert fingerprints are required for custom android application!");
}
if (iosConfig != null && iosConfig.isEnabled() && !mobileAppSettings.isUseDefaultApp() && iosConfig.getAppId() == null) {
throw new DataValidationException("Application id is required for custom ios application!");
}
}
}

61
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java

@ -0,0 +1,61 @@
/**
* 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.
*/
package org.thingsboard.server.dao.sql.mobile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.mobile.MobileAppSettingsDao;
import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.UUID;
@Component
@Slf4j
@SqlDao
public class JpaMobileAppSettingsDao extends JpaAbstractDao<MobileAppSettingsEntity, MobileAppSettings> implements MobileAppSettingsDao {
@Autowired
private MobileAppSettingsRepository mobileAppSettingsRepository;
@Override
public MobileAppSettings findByTenantId(TenantId tenantId) {
return DaoUtil.getData(mobileAppSettingsRepository.findByTenantId(tenantId.getId()));
}
@Override
public void removeByTenantId(TenantId tenantId) {
mobileAppSettingsRepository.deleteByTenantId(tenantId.getId());
}
@Override
protected Class<MobileAppSettingsEntity> getEntityClass() {
return MobileAppSettingsEntity.class;
}
@Override
protected JpaRepository<MobileAppSettingsEntity, UUID> getRepository() {
return mobileAppSettingsRepository;
}
}

36
dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java

@ -0,0 +1,36 @@
/**
* 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.
*/
package org.thingsboard.server.dao.sql.mobile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity;
import java.util.UUID;
public interface MobileAppSettingsRepository extends JpaRepository<MobileAppSettingsEntity, UUID> {
MobileAppSettingsEntity findByTenantId(@Param("tenantId") UUID tenantId);
@Transactional
@Modifying
@Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId")
void deleteByTenantId(@Param("tenantId") UUID tenantId);
}

6
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -38,6 +38,9 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -78,6 +81,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
private NotificationSettingsService notificationSettingsService;
@Autowired
private MobileAppSettingsService mobileAppSettingsService;
@Autowired
private TenantDataValidator tenantValidator;
@Autowired
protected TbTransactionalCache<TenantId, Boolean> existsTenantCache;
@ -161,6 +166,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
userService.deleteAllByTenantId(tenantId);
adminSettingsService.deleteAdminSettingsByTenantId(tenantId);
mobileAppSettingsService.deleteByTenantId(tenantId);
notificationSettingsService.deleteNotificationSettings(tenantId);
tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true));

13
dao/src/main/resources/sql/schema-entities.sql

@ -895,4 +895,15 @@ CREATE TABLE IF NOT EXISTS queue_stats (
queue_name varchar(255) NOT NULL,
service_id varchar(255) NOT NULL,
CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id)
);
);
CREATE TABLE IF NOT EXISTS mobile_app_settings (
id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
use_default_app boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
qr_code_config VARCHAR(100000),
CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
);

45
ui-ngx/src/app/core/http/mobile-app.service.ts

@ -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 { MobileAppQRCodeSettings } from '@shared/models/mobile-app.models';
@Injectable({
providedIn: 'root'
})
export class MobileAppService {
constructor(
private http: HttpClient
) {
}
public getMobileAppSettings(config?: RequestConfig): Observable<MobileAppQRCodeSettings> {
return this.http.get<MobileAppQRCodeSettings>(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config));
}
public saveMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings, config?: RequestConfig): Observable<MobileAppQRCodeSettings> {
return this.http.post<MobileAppQRCodeSettings>(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config));
}
public getMobileAppDeepLink( config?: RequestConfig): Observable<string> {
return this.http.get<string>(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config));
}
}

8
ui-ngx/src/app/core/services/menu.service.ts

@ -245,6 +245,14 @@ export class MenuService {
path: '/settings/queues',
icon: 'swap_calls'
},
{
id: 'mobile_app_settings',
name: 'admin.mobile-app.mobile-app',
fullName: 'admin.mobile-app.mobile-app',
type: 'link',
path: '/settings/mobile-app',
icon: 'smartphone'
}
]
},
{

5
ui-ngx/src/app/core/services/utils.service.ts

@ -30,7 +30,7 @@ import {
guid,
hashCode,
isDefined,
isDefinedAndNotNull,
isDefinedAndNotNull, isNotEmptyStr,
isString,
isUndefined,
objToBase64,
@ -386,8 +386,7 @@ export class UtilsService {
this.window.performance.now() : Date.now();
}
public getQueryParam(name: string): string {
const url = this.window.location.href;
public getQueryParam(name: string, url = this.window.location.href): string {
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);

50
ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts

@ -0,0 +1,50 @@
///
/// 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 {
AndroidConfig,
BadgePosition,
BadgeStyle,
IosConfig,
QRCodeConfig
} from '@shared/models/mobile-app.models';
export type MobileAppQrCodeWidgetSettings = {
useDefaultApp: boolean;
androidConfig: AndroidConfig;
iosConfig: IosConfig;
qrCodeConfig: Omit<QRCodeConfig, 'showOnHomePage'>;
}
export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = {
useDefaultApp: true,
androidConfig: {
enabled: true,
appPackage: '',
sha256CertFingerprints: ''
},
iosConfig: {
enabled: true,
appId: ''
},
qrCodeConfig: {
badgeEnabled: true,
badgeStyle: BadgeStyle.ORIGINAL,
badgePosition: BadgePosition.RIGHT,
qrCodeLabelEnabled: true,
qrCodeLabel: 'Scan to connect or download mobile app'
}
}

41
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html

@ -0,0 +1,41 @@
<!--
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 && !ctx" 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"
[class.row]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.RIGHT"
[class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT">
<!-- <canvas #canvas class="tb-qrcode"></canvas>-->
<canvas #canvas class="tb-qrcode"></canvas>
<div *ngIf="mobileAppSettings?.qrCodeConfig.badgeEnabled"
class="tb-flex column tb-badge-container"
[class.tb-badge-container]="!previewMode && !ctx">
<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>

57
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss

@ -0,0 +1,57 @@
/**
* 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 {
height: 100%;
display: flex;
flex-direction: column;
background-color: transparent;
}
.tb-title {
padding-bottom: 12px;
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-md} {
display: none;
}
}

114
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts

@ -0,0 +1,114 @@
///
/// 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, MobileAppQRCodeSettings } 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';
import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models';
@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: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings) {
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: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings;
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.mobileAppSettings = this.ctx.settings;
} else {
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: 100 });
// QRCode.toCanvas(this.canvasRef.nativeElement, link);
// this.canvasRef.nativeElement.style.width = '4.6vw';
// this.canvasRef.nativeElement.style.height = '4.6vw';
});
}
}

130
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html

@ -0,0 +1,130 @@
<!--
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.
-->
<section class="tb-widget-settings tb-form-panel no-border no-padding" [formGroup]="mobileAppQRCodeWidgetSettingsForm" fxLayout="column">
<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="!mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value && mobileAppQRCodeWidgetSettingsForm.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="mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').hasError('required')
&& mobileAppQRCodeWidgetSettingsForm.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="!mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value && mobileAppQRCodeWidgetSettingsForm.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="mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').hasError('required')
&& mobileAppQRCodeWidgetSettingsForm.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="!mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value && mobileAppQRCodeWidgetSettingsForm.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="mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').hasError('required')
&& mobileAppQRCodeWidgetSettingsForm.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-form-panel stroked no-padding no-gap">
<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">
<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>
</section>

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss

@ -0,0 +1,19 @@
/**
* 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;
}

156
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts

@ -0,0 +1,156 @@
///
/// 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 } from "@angular/core";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.models";
import { AppState } from '@core/core.state';
import { Store } from "@ngrx/store";
import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models';
import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models';
@Component({
selector: 'tb-mobile-app-qr-code-widget-settings',
templateUrl: './mobile-app-qr-code-widget-settings.component.html',
styleUrls: ['/mobile-app-qr-code-widget-settings.component.scss', './../widget-settings.scss']
})
export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsComponent {
mobileAppQRCodeWidgetSettingsForm: UntypedFormGroup;
badgePositionTranslationsMap = badgePositionTranslationsMap;
badgeStyleTranslationsMap = badgeStyleTranslationsMap;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.mobileAppQRCodeWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...mobileAppQrCodeWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({
useDefaultApp: [settings.useDefaultApp, []],
androidConfig: this.fb.group({
enabled: [settings.androidConfig.enabled, []],
appPackage: [settings.androidConfig.appPackage, []],
sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, []]
}),
iosConfig: this.fb.group({
enabled: [settings.iosConfig.enabled, []],
appId: [settings.iosConfig.appId, []]
}),
qrCodeConfig: this.fb.group({
badgeEnabled: [settings.qrCodeConfig.badgeEnabled, []],
badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}, []],
badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}, []],
qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled, []],
qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, []]
})
});
}
protected validatorTriggers(): string[] {
return ['useDefaultApp', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled'];
}
protected updateValidators(emitEvent: boolean) {
const useDefaultApp = this.mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value;
const androidEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled').value;
const iosEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled').value;
const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value;
const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value;
if (useDefaultApp) {
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators();
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators();
this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators();
} else {
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]);
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]);
this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]);
}
if (androidEnabled) {
if (!useDefaultApp) {
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]);
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]);
}
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false});
if (badgeEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable();
}
} else {
if (!useDefaultApp) {
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators();
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators();
}
if (!iosEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false});
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable();
}
}
if (iosEnabled) {
if (!useDefaultApp) {
this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]);
}
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false});
if (badgeEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable();
}
} else {
if (!useDefaultApp) {
this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators();
}
if (!androidEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false});
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable();
}
}
if (badgeEnabled) {
if (androidEnabled || iosEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable();
}
} else {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable();
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable();
}
if (qrCodeLabelEnabled) {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable();
} else {
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable();
}
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').updateValueAndValidity({emitEvent});
this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity({emitEvent});
this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').updateValueAndValidity({emitEvent});
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -354,10 +354,14 @@ import {
import {
RadarChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/radar-chart-widget-settings.component';
import {
MobileAppQrCodeWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component';
@NgModule({
declarations: [
QrCodeWidgetSettingsComponent,
MobileAppQrCodeWidgetSettingsComponent,
TimeseriesTableWidgetSettingsComponent,
TimeseriesTableKeySettingsComponent,
TimeseriesTableLatestKeySettingsComponent,
@ -490,6 +494,7 @@ import {
],
exports: [
QrCodeWidgetSettingsComponent,
MobileAppQrCodeWidgetSettingsComponent,
TimeseriesTableWidgetSettingsComponent,
TimeseriesTableKeySettingsComponent,
TimeseriesTableLatestKeySettingsComponent,
@ -620,6 +625,7 @@ export class WidgetSettingsModule {
export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsComponent>} = {
'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent,
'tb-mobile-app-qr-code-widget-settings': MobileAppQrCodeWidgetSettingsComponent,
'tb-timeseries-table-widget-settings': TimeseriesTableWidgetSettingsComponent,
'tb-timeseries-table-key-settings': TimeseriesTableKeySettingsComponent,
'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent,

3
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -92,6 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c
import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component';
import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component';
import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component';
import { MobileAppQrcodeWidgetComponent } from '@home/components/widget/lib/mobile-app-qrcode-widget.component';
@NgModule({
declarations:
@ -112,6 +113,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent,
LegendComponent,
@ -176,6 +178,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,

14
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -40,6 +40,7 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component';
import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -270,6 +271,19 @@ const routes: Routes = [
}
}
},
{
path: 'mobile-app',
component: MobileAppSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.mobile-app.mobile-app',
breadcrumb: {
label: 'admin.mobile-app.mobile-app',
icon: 'smartphone'
}
}
},
{
path: 'security-settings',
redirectTo: '/security-settings/general'

18
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -33,6 +33,8 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component';
import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component';
import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';
@NgModule({
declarations:
@ -49,13 +51,15 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,
TwoFactorAuthSettingsComponent
TwoFactorAuthSettingsComponent,
MobileAppSettingsComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
AdminRoutingModule
]
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
AdminRoutingModule,
WidgetComponentsModule
]
})
export class AdminModule { }

161
ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html

@ -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>

28
ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss

@ -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);
}
}

198
ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts

@ -0,0 +1,198 @@
///
/// 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,
MobileAppQRCodeSettings
} from '@shared/models/mobile-app.models';
import { AuthService } from '@core/auth/auth.service';
@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: MobileAppQRCodeSettings;
private readonly destroy$ = new Subject<void>();
badgePositionTranslationsMap = badgePositionTranslationsMap;
badgeStyleTranslationsMap = badgeStyleTranslationsMap;
constructor(protected store: Store<AppState>,
private mobileAppService: MobileAppService,
private 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: MobileAppQRCodeSettings): 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;
}
}

81
ui-ngx/src/app/shared/models/mobile-app.models.ts

@ -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 MobileAppQRCodeSettings {
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'
}]
]);

35
ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

46
ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg

@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

46
ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg

@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z" style="fill: #fff"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.99671,19.88935a5.14625,5.14625,0,0,1,2.45058-4.31771,5.26776,5.26776,0,0,0-4.15039-2.24376c-1.74624-.1833-3.43913,1.04492-4.329,1.04492-.90707,0-2.27713-1.02672-3.75247-.99637a5.52735,5.52735,0,0,0-4.65137,2.8367c-2.01111,3.482-.511,8.59939,1.41551,11.414.96388,1.37823,2.09037,2.91774,3.56438,2.86315,1.4424-.05983,1.98111-.91977,3.7222-.91977,1.72494,0,2.23035.91977,3.73427.88506,1.54777-.02512,2.52292-1.38435,3.453-2.77563a11.39931,11.39931,0,0,0,1.579-3.21589A4.97284,4.97284,0,0,1,24.99671,19.88935Z"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.15611,11.47681a5.06687,5.06687,0,0,0,1.159-3.62989,5.15524,5.15524,0,0,0-3.33555,1.72582,4.82131,4.82131,0,0,0-1.18934,3.4955A4.26259,4.26259,0,0,0,22.15611,11.47681Z"/>
</g>
</g>
<g>
<path d="M42.30178,27.13965h-4.7334l-1.13672,3.35645H34.42678l4.4834-12.418h2.083l4.4834,12.418H43.43752Zm-4.24316-1.54883h3.752L39.961,20.14355H39.9092Z"/>
<path d="M55.1592,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.64455,21.34766,55.1592,23.16406,55.1592,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30178,29.01563,53.249,27.81934,53.249,25.96973Z"/>
<path d="M65.12453,25.96973c0,2.81348-1.50635,4.62109-3.77881,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C63.6094,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91064,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26662,29.01563,63.21389,27.81934,63.21389,25.96973Z"/>
<path d="M71.70949,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51758-3.61426,2.625,0,4.42383,1.47168,4.48438,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60547,1.626,3.60547,3.44238,0,2.32324-1.84961,3.77832-4.793,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z"/>
<path d="M86.064,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72512,30.6084,86.064,28.82617,86.064,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40137,1.16211-2.40137,3.10742c0,1.96191.89551,3.10645,2.40137,3.10645S92.7593,27.93164,92.7593,25.96973Z"/>
<path d="M96.18508,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z"/>
<path d="M109.38332,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10109,25.13477Z"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

53
ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

@ -1143,6 +1143,47 @@
"row": 0,
"col": 0,
"id": "8e71a398-caf5-540d-cec5-6e5dc264343e"
},
"b9d704a2-f579-dbff-f123-f953d92081fd": {
"typeFullFqn": "system.home_page_widgets.home_mobile_app_qr_code",
"type": "static",
"sizeX": 6,
"sizeY": 3,
"config": {
"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": {},
"title": "Mobile app QR code",
"dropShadow": false,
"enableFullscreen": false
},
"row": 0,
"col": 0,
"id": "b9d704a2-f579-dbff-f123-f953d92081fd"
}
},
"states": {
@ -1169,7 +1210,7 @@
},
"a23185ad-dc46-806c-0e50-5b21fb080ace": {
"sizeX": 35,
"sizeY": 58,
"sizeY": 42,
"row": 0,
"col": 85,
"mobileHide": true
@ -1212,6 +1253,14 @@
"row": 0,
"col": 43,
"mobileOrder": 0
},
"b9d704a2-f579-dbff-f123-f953d92081fd": {
"sizeX": 35,
"sizeY": 16,
"row": 42,
"col": 85,
"mobileOrder": 5,
"mobileHeight": 7
}
},
"gridSettings": {
@ -1416,4 +1465,4 @@
}
},
"name": "Tenant Administrator Home Page"
}
}

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -419,6 +419,33 @@
"no-auto-commit-entities-prompt": "No entities configured for auto-commit",
"delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?",
"delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.",
"mobile-app": {
"mobile-app": "Mobile app",
"mobile-app-qr-code-widget-settings": "Mobile app QR code widget settings",
"applications": "Applications",
"default": "Default",
"custom": "Custom",
"android": "Android",
"app-package-name": "App package name",
"app-package-name-required": "App package name is required",
"sha256-certificate-fingerprints": "SHA256 certificate fingerprints",
"sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required",
"ios": "iOS",
"app-id": "App ID",
"app-id-required": "App ID is required",
"appearance-on-home-page": "Appearance on Home page",
"enabled": "Enabled",
"disabled": "Disabled",
"badges": "Badges",
"original": "Original",
"white": "White",
"label": "Label",
"right": "Right",
"left": "Left",
"set": "Set",
"preview": "Preview",
"connect-mobile-app": "Connect mobile app"
},
"2fa": {
"2fa": "Two-factor authentication",
"available-providers": "Available providers",

12
ui-ngx/src/form.scss

@ -257,9 +257,15 @@
&.row {
flex-direction: row;
}
&.row-reverse {
flex-direction: row-reverse;
}
&.column {
flex-direction: column;
}
&.center {
justify-content: center;
}
&.flex-start {
justify-content: flex-start;
}
@ -278,6 +284,12 @@
&.fill-width {
width: 100%;
}
&.fill-height {
height: 100%;
}
&.shrink {
flex: 0;
}
}
.tb-form-panel, .tb-form-row {

Loading…
Cancel
Save