committed by
GitHub
70 changed files with 2968 additions and 122 deletions
File diff suppressed because one or more lines are too long
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { MobileAppSettings } from '@shared/models/mobile-app.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class MobileAppService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public getMobileAppSettings(config?: RequestConfig): Observable<MobileAppSettings> { |
||||
|
return this.http.get<MobileAppSettings>(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable<MobileAppSettings> { |
||||
|
return this.http.post<MobileAppSettings>(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getMobileAppDeepLink(config?: RequestConfig): Observable<string> { |
||||
|
return this.http.get<string>(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
///
|
||||
|
/// 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 interface MobileAppQrCodeWidgetSettings { |
||||
|
useSystemSettings: boolean; |
||||
|
androidConfig: Pick<AndroidConfig, 'enabled'>; |
||||
|
iosConfig: Pick<IosConfig, 'enabled'>; |
||||
|
qrCodeConfig: Omit<QRCodeConfig, 'showOnHomePage'>; |
||||
|
} |
||||
|
|
||||
|
export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = { |
||||
|
useSystemSettings: true, |
||||
|
androidConfig: { |
||||
|
enabled: true |
||||
|
}, |
||||
|
iosConfig: { |
||||
|
enabled: true |
||||
|
}, |
||||
|
qrCodeConfig: { |
||||
|
badgeEnabled: true, |
||||
|
badgeStyle: BadgeStyle.ORIGINAL, |
||||
|
badgePosition: BadgePosition.RIGHT, |
||||
|
qrCodeLabelEnabled: true, |
||||
|
qrCodeLabel: 'Scan to connect or download mobile app' |
||||
|
} |
||||
|
} |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<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> |
||||
|
<div class="tb-flex column" *ngIf="mobileAppSettings?.qrCodeConfig.badgeEnabled && showBadgeContainer"> |
||||
|
<ng-container *ngIf="mobileAppSettings.qrCodeConfig.badgeStyle === badgeStyle.ORIGINAL;else white"> |
||||
|
<img *ngIf="mobileAppSettings.iosConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.ORIGINAL).iOS"> |
||||
|
<img *ngIf="mobileAppSettings.androidConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.ORIGINAL).android"> |
||||
|
</ng-container> |
||||
|
<ng-template #white> |
||||
|
<img *ngIf="mobileAppSettings.iosConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.WHITE).iOS"> |
||||
|
<img *ngIf="mobileAppSettings.androidConfig.enabled" [src]="badgeStyleURLMap.get(badgeStyle.WHITE).android"> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="mobileAppSettings?.qrCodeConfig.qrCodeLabelEnabled" class="tb-qrcode-label"> |
||||
|
{{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,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. |
||||
|
*/ |
||||
|
|
||||
|
@import '../../../../../../scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
|
||||
|
.tb-qrcode-label { |
||||
|
font-size: 14px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
|
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-qrcode { |
||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08); |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,124 @@ |
|||||
|
///
|
||||
|
/// 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 { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; |
||||
|
import { MobileAppService } from '@core/http/mobile-app.service'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { MINUTE } from '@shared/models/time/time.models'; |
||||
|
import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { ResizeObserver } from '@juggle/resize-observer'; |
||||
|
|
||||
|
@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, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
set mobileAppSettings(settings: MobileAppSettings | MobileAppQrCodeWidgetSettings) { |
||||
|
if (settings) { |
||||
|
this.mobileAppSettingsValue = settings; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
get mobileAppSettings(): MobileAppSettings | MobileAppQrCodeWidgetSettings { |
||||
|
return this.mobileAppSettingsValue; |
||||
|
} |
||||
|
|
||||
|
@ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>; |
||||
|
|
||||
|
private readonly destroy$ = new Subject<void>(); |
||||
|
private widgetResize$: ResizeObserver; |
||||
|
|
||||
|
badgeStyle = BadgeStyle; |
||||
|
badgePosition = BadgePosition; |
||||
|
badgeStyleURLMap = badgeStyleURLMap; |
||||
|
showBadgeContainer = true; |
||||
|
|
||||
|
private mobileAppSettingsValue: MobileAppSettings | MobileAppQrCodeWidgetSettings; |
||||
|
private deepLinkTTL: number; |
||||
|
private deepLinkTTLTimeoutID: NodeJS.Timeout; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected cd: ChangeDetectorRef, |
||||
|
private mobileAppService: MobileAppService, |
||||
|
private utilsService: UtilsService, |
||||
|
private elementRef: ElementRef) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
if (!this.mobileAppSettings) { |
||||
|
if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) { |
||||
|
this.mobileAppSettings = this.ctx.settings; |
||||
|
} else { |
||||
|
this.mobileAppService.getMobileAppSettings().subscribe((settings => { |
||||
|
this.mobileAppSettings = settings; |
||||
|
this.cd.markForCheck(); |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
this.initMobileAppQRCode(); |
||||
|
this.widgetResize$ = new ResizeObserver(() => { |
||||
|
const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250; |
||||
|
if (showHideBadgeContainer !== this.showBadgeContainer) { |
||||
|
this.showBadgeContainer = showHideBadgeContainer; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
this.widgetResize$.observe(this.elementRef.nativeElement); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.widgetResize$) { |
||||
|
this.widgetResize$.disconnect(); |
||||
|
} |
||||
|
super.ngOnDestroy(); |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
clearTimeout(this.deepLinkTTLTimeoutID); |
||||
|
} |
||||
|
|
||||
|
private initMobileAppQRCode() { |
||||
|
if (this.deepLinkTTLTimeoutID) { |
||||
|
clearTimeout(this.deepLinkTTLTimeoutID); |
||||
|
this.deepLinkTTLTimeoutID = null; |
||||
|
} |
||||
|
this.mobileAppService.getMobileAppDeepLink().subscribe(link => { |
||||
|
this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; |
||||
|
this.updateQRCode(link); |
||||
|
this.deepLinkTTLTimeoutID = setTimeout(() => this.initMobileAppQRCode(), this.deepLinkTTL); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateQRCode(link: string) { |
||||
|
import('qrcode').then((QRCode) => { |
||||
|
QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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"> |
||||
|
<div class="tb-form-panel no-padding"> |
||||
|
<div class="tb-form-row no-border no-padding-bottom"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="useSystemSettings" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.use-system-settings' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel" *ngIf="!mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value"> |
||||
|
<div class="tb-form-panel-title" translate>admin.mobile-app.applications</div> |
||||
|
<div class="tb-form-panel stroked no-padding no-gap"> |
||||
|
<div class="tb-form-row no-border no-padding-bottom"> |
||||
|
<mat-slide-toggle class="mat-slide" [formControl]="mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled')" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.android' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</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" [formControl]="mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled')" (click)="$event.stopPropagation()"> |
||||
|
{{ 'admin.mobile-app.ios' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel" formGroupName="qrCodeConfig" *ngIf="!mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value"> |
||||
|
<div class="tb-form-panel-title" translate>admin.mobile-app.appearance</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 fixed-title-width-230" 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 fixed-title-width-230" 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-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'admin.mobile-app.label-required' | translate" |
||||
|
*ngIf="mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') |
||||
|
&& mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,104 @@ |
|||||
|
///
|
||||
|
/// 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: ['./../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({ |
||||
|
useSystemSettings: [settings.useSystemSettings], |
||||
|
androidConfig: this.fb.group({ |
||||
|
enabled: [settings.androidConfig.enabled], |
||||
|
}), |
||||
|
iosConfig: this.fb.group({ |
||||
|
enabled: [settings.iosConfig.enabled], |
||||
|
}), |
||||
|
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, [Validators.required]] |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['useSystemSettings', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean) { |
||||
|
const useSystemSettings = this.mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').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 (!androidEnabled && !iosEnabled) { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
if (androidEnabled || iosEnabled) { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); |
||||
|
if (badgeEnabled) { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (qrCodeLabelEnabled && !useSystemSettings) { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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="fixed-title-width-230">{{ '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="fixed-title-width-230">{{ '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="fixed-title-width-230">{{ '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 fixed-title-width-230" 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 fixed-title-width-230" 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-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'admin.mobile-app.label-required' | translate" |
||||
|
*ngIf="mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') |
||||
|
&& mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</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 |
||||
|
[mobileAppSettings]="mobileAppSettingsForm.getRawValue()"> |
||||
|
</tb-mobile-app-qrcode-widget> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-flex row flex-end" style="margin-top: 16px"> |
||||
|
<button mat-button mat-raised-button color="primary" (click)="save()" |
||||
|
[disabled]="(isLoading$ | async) || mobileAppSettingsForm.invalid || !mobileAppSettingsForm.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
@ -0,0 +1,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. |
||||
|
*/ |
||||
|
|
||||
|
.tb-qrcode-preview { |
||||
|
background-color: rgba(0, 0, 0, 0.04); |
||||
|
box-shadow: none; |
||||
|
|
||||
|
.tb-form-panel-title { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,193 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnDestroy } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
import { Subject, takeUntil } from 'rxjs'; |
||||
|
import { MobileAppService } from '@core/http/mobile-app.service'; |
||||
|
import { |
||||
|
BadgePosition, |
||||
|
badgePositionTranslationsMap, |
||||
|
BadgeStyle, |
||||
|
badgeStyleTranslationsMap, |
||||
|
MobileAppSettings |
||||
|
} from '@shared/models/mobile-app.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mobile-app-settings', |
||||
|
templateUrl: './mobile-app-settings.component.html', |
||||
|
styleUrls: ['mobile-app-settings.component.scss', './settings-card.scss'] |
||||
|
}) |
||||
|
export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { |
||||
|
|
||||
|
mobileAppSettingsForm: FormGroup; |
||||
|
|
||||
|
mobileAppSettings: MobileAppSettings; |
||||
|
|
||||
|
private readonly destroy$ = new Subject<void>(); |
||||
|
|
||||
|
badgePositionTranslationsMap = badgePositionTranslationsMap; |
||||
|
badgeStyleTranslationsMap = badgeStyleTranslationsMap; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private mobileAppService: MobileAppService, |
||||
|
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').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); |
||||
|
} |
||||
|
if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
this.androidEnableChanged(value); |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
this.iosEnableChanged(value); |
||||
|
}); |
||||
|
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({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(value => { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
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: [{value: '', disabled: true}, [Validators.required]], |
||||
|
sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]] |
||||
|
}), |
||||
|
iosConfig: this.fb.group({ |
||||
|
enabled: [true], |
||||
|
appId: [{value: '', disabled: true}, [Validators.required]] |
||||
|
}), |
||||
|
qrCodeConfig: this.fb.group({ |
||||
|
showOnHomePage: [true], |
||||
|
badgeEnabled: [true], |
||||
|
badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}], |
||||
|
badgePosition: [{value: BadgePosition.RIGHT, disabled: true}], |
||||
|
qrCodeLabelEnabled: [true], |
||||
|
qrCodeLabel: ['', [Validators.required]] |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { |
||||
|
this.mobileAppSettings = {...mobileAppSettings}; |
||||
|
this.mobileAppSettingsForm.reset(this.mobileAppSettings); |
||||
|
} |
||||
|
|
||||
|
private androidEnableChanged(value: boolean): void { |
||||
|
if (value) { |
||||
|
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.updateBadgeControls(value); |
||||
|
} |
||||
|
|
||||
|
private iosEnableChanged(value: boolean): void { |
||||
|
if (value) { |
||||
|
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.updateBadgeControls(value); |
||||
|
} |
||||
|
|
||||
|
private updateBadgeControls(value: boolean) { |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { |
||||
|
if (value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); |
||||
|
if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); |
||||
|
} |
||||
|
} else { |
||||
|
if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value && !this.mobileAppSettingsForm.get('androidConfig.enabled').value) { |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); |
||||
|
this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; |
||||
|
this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) |
||||
|
.subscribe((settings) => this.processMobileAppSettings(settings)); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.mobileAppSettingsForm; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
///
|
||||
|
/// 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 { HasTenantId } from '@shared/models/entity.models'; |
||||
|
|
||||
|
export interface MobileAppSettings extends HasTenantId { |
||||
|
useDefaultApp: boolean; |
||||
|
androidConfig: AndroidConfig; |
||||
|
iosConfig: IosConfig; |
||||
|
qrCodeConfig: QRCodeConfig; |
||||
|
} |
||||
|
|
||||
|
export interface AndroidConfig { |
||||
|
enabled: boolean; |
||||
|
appPackage: string; |
||||
|
sha256CertFingerprints: string |
||||
|
} |
||||
|
|
||||
|
export interface IosConfig { |
||||
|
enabled: boolean; |
||||
|
appId: string; |
||||
|
} |
||||
|
|
||||
|
export interface QRCodeConfig { |
||||
|
showOnHomePage: boolean; |
||||
|
badgeEnabled: boolean; |
||||
|
badgePosition: BadgePosition; |
||||
|
badgeStyle: BadgeStyle; |
||||
|
qrCodeLabelEnabled: boolean; |
||||
|
qrCodeLabel: string; |
||||
|
} |
||||
|
|
||||
|
export interface MobileOSBadgeURL { |
||||
|
iOS: string; |
||||
|
android: string; |
||||
|
} |
||||
|
|
||||
|
export enum BadgePosition { |
||||
|
RIGHT = 'RIGHT', |
||||
|
LEFT = 'LEFT' |
||||
|
} |
||||
|
|
||||
|
export const badgePositionTranslationsMap = new Map<BadgePosition, string>([ |
||||
|
[BadgePosition.RIGHT, 'admin.mobile-app.right'], |
||||
|
[BadgePosition.LEFT, 'admin.mobile-app.left'] |
||||
|
]); |
||||
|
|
||||
|
export enum BadgeStyle { |
||||
|
ORIGINAL = 'ORIGINAL', |
||||
|
WHITE = 'WHITE' |
||||
|
} |
||||
|
|
||||
|
export const badgeStyleTranslationsMap = new Map<BadgeStyle, string>([ |
||||
|
[BadgeStyle.ORIGINAL, 'admin.mobile-app.original'], |
||||
|
[BadgeStyle.WHITE, 'admin.mobile-app.white'] |
||||
|
]); |
||||
|
|
||||
|
export const badgeStyleURLMap = new Map<BadgeStyle, MobileOSBadgeURL>([ |
||||
|
[BadgeStyle.ORIGINAL, { |
||||
|
iOS: 'assets/android-ios-stores-badges/ios_store_en_black_badge.svg', |
||||
|
android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' |
||||
|
}], |
||||
|
[BadgeStyle.WHITE, { |
||||
|
iOS: 'assets/android-ios-stores-badges/ios_store_en_white_badge.svg', |
||||
|
android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' |
||||
|
}] |
||||
|
]); |
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
Loading…
Reference in new issue