From 02623f005f3e4b3b0021df943243022bb066cef2 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 10:35:40 +0100 Subject: [PATCH 01/23] console: Add fleet options to gateway claiming form --- pkg/webui/components/form/field/index.js | 15 +++- .../gateway-claim-form-section/index.js | 69 +++++++++++++++---- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/pkg/webui/components/form/field/index.js b/pkg/webui/components/form/field/index.js index 897449efbe..b6e920d5eb 100644 --- a/pkg/webui/components/form/field/index.js +++ b/pkg/webui/components/form/field/index.js @@ -77,6 +77,7 @@ const FormField = props => { component: Component, decode, description, + descriptionValues, disabled: inputDisabled, encode, fieldWidth, @@ -92,6 +93,7 @@ const FormField = props => { valueSetter, onChange, onBlur, + showTitle, } = props const { @@ -222,7 +224,12 @@ const FormField = props => { ) : showDescription ? ( - + ) : null const fieldComponentProps = { @@ -253,7 +260,7 @@ const FormField = props => { return (
- {hasTitle && ( + {hasTitle && showTitle && (
Managed Gateway. To claim this gateway, please use the owner token printed on the inside of the mounting lid or scan the QR code to claim instantly.', + 'We detected a Managed gateway. To claim this gateway, please use the owner token printed on the gateway, or the gateway fleet owner token.', + fleet: 'Fleet', + fleetInfo: 'Adding a gateway to a fleet will blablabla.', + fleetTokenInfo: 'Your fleet token is available in your account.', }) const initialValues = { @@ -49,10 +56,23 @@ const initialValues = { target_gateway_server_address: gsEnabled ? getHostFromUrl(gsBaseURL) : '', } +const ownerTokenTypes = [ + { name: 'gateway', title: sharedMessages.gateway }, + { name: 'fleet', title: m.fleet }, +] + const GatewayClaimFormSection = () => { const { values, addToFieldRegistry, removeFromFieldRegistry } = useFormikContext() const isManaged = values._inputMethod === 'managed' - const withQRdata = values._withQRdata + const isFleet = values._isFleet + + const [activeOwnerTokenType, setActiveOwnerTokenType] = React.useState( + isFleet ? 'fleet' : 'gateway', + ) + + const onOwnerTokenTypeChange = useCallback(value => { + setActiveOwnerTokenType(value) + }, []) // Register hidden fields so they don't get cleaned. useEffect(() => { @@ -64,27 +84,46 @@ const GatewayClaimFormSection = () => { return ( <> {isManaged && ( - - + + {txt}, + }} + className="mb-0" + /> + + + {txt}, - }} - className="mb-0" + className="w-content p-0 mb-cs-xs mt-cs-xxs border-none br-m gap-0 fs-s" /> - + {activeOwnerTokenType === 'fleet' && ( + + )} + )} From e09e712b64cbd1634af5fefb1bbc195d3d38e1b7 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 10:47:29 +0100 Subject: [PATCH 02/23] console: Add fleets to qr code modal --- pkg/webui/components/qr-modal-button/index.js | 81 ++++++++++++++++++- pkg/webui/components/tabs/index.js | 4 + pkg/webui/components/tabs/tab/index.js | 4 + pkg/webui/components/tabs/tab/tab.styl | 4 + .../gateway-claim-form-section/index.js | 6 +- .../validation-schema.js | 1 + .../qr-scan-section/index.js | 12 ++- pkg/webui/lib/shared-messages.js | 2 + 8 files changed, 106 insertions(+), 8 deletions(-) diff --git a/pkg/webui/components/qr-modal-button/index.js b/pkg/webui/components/qr-modal-button/index.js index 6904ad0388..28b73f5079 100644 --- a/pkg/webui/components/qr-modal-button/index.js +++ b/pkg/webui/components/qr-modal-button/index.js @@ -15,9 +15,11 @@ import React, { useCallback } from 'react' import { defineMessages } from 'react-intl' -import { IconCamera } from '@ttn-lw/components/icon' +import { IconCamera, IconPlus } from '@ttn-lw/components/icon' import Link from '@ttn-lw/components/link' import ModalButton from '@ttn-lw/components/button/modal-button' +import Button from '@ttn-lw/components/button' +import Input from '@ttn-lw/components/input' import Message from '@ttn-lw/lib/components/message' import ErrorMessage from '@ttn-lw/lib/components/error-message' @@ -37,10 +39,38 @@ const QrScanDoc = ( const m = defineMessages({ scanContinue: 'Please scan the QR code to continue. {qrScanDoc}', apply: 'Apply', + fleetToken: 'Fleet token', + addFleet: 'Add Fleet', }) const QRModalButton = props => { - const { message, onApprove, onCancel, onRead, qrData, invalidMessage } = props + const { message, onApprove, onCancel, onRead, qrData, setQrData, invalidMessage } = props + const [isAddFleet, setIsAddFleet] = React.useState(undefined) + const [fleetToken, setFleetToken] = React.useState('') + + const handleAddFleet = useCallback(() => { + setIsAddFleet(true) + }, []) + + const handleRemoveFleet = useCallback(() => { + setIsAddFleet(false) + setFleetToken('') + setQrData({ + ...qrData, + gateway: { ...qrData.gateway, _fleet_token: undefined }, + }) + }, [qrData, setQrData]) + + const handleFleetTokenChange = useCallback( + value => { + setFleetToken(value) + setQrData({ + ...qrData, + gateway: { ...qrData.gateway, _fleet_token: value }, + }) + }, + [qrData, setQrData], + ) const handleRead = useCallback( val => { @@ -53,7 +83,47 @@ const QRModalButton = props => {
{qrData.data ? ( qrData.valid ? ( - + <> + + {qrData.gateway.is_managed && ( + <> + {isAddFleet ? ( +
+
+ +
+ + + +
+ ) : ( +
{ />
diff --git a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js index 19a91b4093..22090dc0e5 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js @@ -74,6 +74,7 @@ const GatewayQRScanSection = () => { ? btoa(gateway.owner_token) : '', }, + _fleet_token: gateway._fleet_token, _isFleet: Boolean(gateway._fleet_token), })) diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index 67cc70faa9..7b96ea12b0 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -509,7 +509,7 @@ export default defineMessages({ resetConfirm: 'Are you sure you want to discard QR code data? The scanned device will not be registered and the form will be reset.', scanSuccess: 'QR code scanned successfully', - scanGatewayQR: 'Scan gateway QR code', + scanGatewayQR: 'Gateway QR code', redirecting: 'Redirecting…', refresh: 'Refresh', reactivateSuccess: 'Successfully reactivated', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index aed5244a9d..de92045201 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -616,7 +616,8 @@ "console.containers.gateway-managed-gateway.wifi-profiles.overview.deleteSuccess": "WiFi profile deleted", "console.containers.gateway-managed-gateway.wifi-profiles.overview.deleteFail": "There was an error and the WiFi profile could not be deleted", "console.containers.gateway-managed-gateway.wifi-profiles.overview.deleteModalMessage": "The profile will not be applicable to gateways anymore and gateways using this profile might lose connectivity. Please make sure to update the gateway settings before deleting this profile.", - "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-claim-form-section.index.claimWarning": "We detected that your gateway is a Managed Gateway. To claim this gateway, please use the owner token printed on the inside of the mounting lid or scan the QR code to claim instantly.", + "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-claim-form-section.index.claimWarning": "We detected a Managed gateway. To claim this gateway, please use the owner token printed on the gateway, or the gateway fleet owner token.", + "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-claim-form-section.index.fleet": "Fleet", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.requireAuthenticatedConnectionDescription": "Select which information can be seen by other network participants, including {packetBrokerURL}", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.shareGatewayInfoDescription": "Choose this option eg. if your gateway is powered by {loraBasicStationURL}", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.validation-schema.validateEntry": "There must be at least one selected frequency plan ID.", @@ -1657,7 +1658,7 @@ "lib.shared-messages.qrCodeDataReset": "Reset QR code data", "lib.shared-messages.resetConfirm": "Are you sure you want to discard QR code data? The scanned device will not be registered and the form will be reset.", "lib.shared-messages.scanSuccess": "QR code scanned successfully", - "lib.shared-messages.scanGatewayQR": "Scan gateway QR code", + "lib.shared-messages.scanGatewayQR": "Gateway QR code", "lib.shared-messages.redirecting": "Redirecting…", "lib.shared-messages.refresh": "Refresh", "lib.shared-messages.reactivateSuccess": "Successfully reactivated", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index a54d0ec362..fd3b94dcaf 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -617,6 +617,7 @@ "console.containers.gateway-managed-gateway.wifi-profiles.overview.deleteFail": "", "console.containers.gateway-managed-gateway.wifi-profiles.overview.deleteModalMessage": "", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-claim-form-section.index.claimWarning": "", + "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-claim-form-section.index.fleet": "", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.requireAuthenticatedConnectionDescription": "", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.shareGatewayInfoDescription": "", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.validation-schema.validateEntry": "", diff --git a/pkg/webui/styles/utilities/general.styl b/pkg/webui/styles/utilities/general.styl index dce6c9ff27..2f8edc5d13 100644 --- a/pkg/webui/styles/utilities/general.styl +++ b/pkg/webui/styles/utilities/general.styl @@ -165,6 +165,12 @@ .h-vh height: 100vh !important + .h-l + height: $cs.l !important + + .h-content + height: fit-content !important + for $num in (1..9) $percentage = percentage($num / 10) .w-{$num}0 @@ -474,3 +480,18 @@ .border-regular border: 1px solid var(--c-border-neutral-light) !important + + .no-wrap + white-space: nowrap !important + + .flex-basis-1 + flex-basis: 1rem !important + + .hover-underline:hover + text-decoration: underline !important + + // Responsive flex-basis. + for $name, $width in $bp + +media-query($width) + .{$name}\\:flex-basis-1 + flex-basis: 1rem !important From 2fe05d0994b016bfc54d49510d169945480683dc Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 12:09:13 +0100 Subject: [PATCH 05/23] console: Fix toggle --- .../gateway-claim-form-section/index.js | 22 ++++++++++++++----- .../qr-scan-section/index.js | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js index abb8778157..30e0416a76 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js @@ -60,7 +60,7 @@ const ownerTokenTypes = [ ] const GatewayClaimFormSection = () => { - const { values, addToFieldRegistry, removeFromFieldRegistry } = useFormikContext() + const { values, addToFieldRegistry, removeFromFieldRegistry, setFieldValue } = useFormikContext() const isManaged = values._inputMethod === 'managed' const isFleet = values._isFleet @@ -68,10 +68,22 @@ const GatewayClaimFormSection = () => { isFleet ? 'fleet' : 'gateway', ) - const onOwnerTokenTypeChange = useCallback(value => { - setActiveOwnerTokenType(value) - }, []) - + const onOwnerTokenTypeChange = useCallback( + value => { + console.log('values', values) + setActiveOwnerTokenType(value) + if (value === 'fleet') { + setFieldValue('authenticated_identifiers.authentication_code', values._fleet_token || '') + } else { + setFieldValue( + 'authenticated_identifiers.authentication_code', + values._owner_token ? values._owner_token : '', + ) + } + }, + [setFieldValue, values], + ) + console.log('values', values) // Register hidden fields so they don't get cleaned. useEffect(() => { const hiddenFields = ['target_gateway_server_address'] diff --git a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js index 22090dc0e5..fc9cd7f52a 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js @@ -58,7 +58,7 @@ const GatewayQRScanSection = () => { const handleQRCodeApprove = useCallback(() => { const { gateway } = qrData - + console.log('gateway from qr', gateway.owner_token) setValues(values => ({ ...values, _withQRdata: true, @@ -74,6 +74,7 @@ const GatewayQRScanSection = () => { ? btoa(gateway.owner_token) : '', }, + _owner_token: gateway.owner_token ? btoa(gateway.owner_token) : '', _fleet_token: gateway._fleet_token, _isFleet: Boolean(gateway._fleet_token), })) From 06167a877d8f08019744e2b36774523dc81d1684 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 12:27:50 +0100 Subject: [PATCH 06/23] console: FIx authentication identifiers encoding --- .../gateway-claim-form-section/index.js | 31 ++++++++++--------- .../qr-scan-section/index.js | 7 ++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js index 30e0416a76..55e1001093 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js @@ -60,7 +60,7 @@ const ownerTokenTypes = [ ] const GatewayClaimFormSection = () => { - const { values, addToFieldRegistry, removeFromFieldRegistry, setFieldValue } = useFormikContext() + const { values, addToFieldRegistry, removeFromFieldRegistry, setValues } = useFormikContext() const isManaged = values._inputMethod === 'managed' const isFleet = values._isFleet @@ -70,20 +70,23 @@ const GatewayClaimFormSection = () => { const onOwnerTokenTypeChange = useCallback( value => { - console.log('values', values) setActiveOwnerTokenType(value) - if (value === 'fleet') { - setFieldValue('authenticated_identifiers.authentication_code', values._fleet_token || '') - } else { - setFieldValue( - 'authenticated_identifiers.authentication_code', - values._owner_token ? values._owner_token : '', - ) - } + setValues(values => ({ + ...values, + authenticated_identifiers: { + ...values.authenticated_identifiers, + authentication_code: + value === 'fleet' + ? btoa(values._fleet_token) + : value === 'gateway' + ? btoa(values._owner_token) + : '', + }, + })) }, - [setFieldValue, values], + [setValues], ) - console.log('values', values) + // Register hidden fields so they don't get cleaned. useEffect(() => { const hiddenFields = ['target_gateway_server_address'] @@ -132,8 +135,8 @@ const GatewayClaimFormSection = () => { tooltipId={tooltipIds.CLAIM_AUTH_CODE} component={Input} description={activeOwnerTokenType === 'fleet' ? sharedMessages.fleetTokenInfo : undefined} - encode={!isFleet ? btoa : undefined} - decode={!isFleet ? atob : undefined} + encode={btoa} + decode={atob} sensitive autoFocus /> diff --git a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js index fc9cd7f52a..f391acb75a 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js @@ -58,7 +58,6 @@ const GatewayQRScanSection = () => { const handleQRCodeApprove = useCallback(() => { const { gateway } = qrData - console.log('gateway from qr', gateway.owner_token) setValues(values => ({ ...values, _withQRdata: true, @@ -69,13 +68,13 @@ const GatewayQRScanSection = () => { authenticated_identifiers: { gateway_eui: gateway.gateway_eui, authentication_code: gateway._fleet_token - ? gateway._fleet_token + ? btoa(gateway._fleet_token) : gateway.owner_token ? btoa(gateway.owner_token) : '', }, - _owner_token: gateway.owner_token ? btoa(gateway.owner_token) : '', - _fleet_token: gateway._fleet_token, + _owner_token: gateway.owner_token ?? '', + _fleet_token: gateway._fleet_token ?? '', _isFleet: Boolean(gateway._fleet_token), })) From aa4430900603d1d18df8060a6e385a5b793617f5 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 13:33:25 +0100 Subject: [PATCH 07/23] console: Fix fleet token logic --- pkg/webui/components/qr-modal-button/index.js | 33 ++++++++++--------- .../gateway-claim-form-section/index.js | 8 ++--- .../qr-scan-section/index.js | 23 ++++++------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/pkg/webui/components/qr-modal-button/index.js b/pkg/webui/components/qr-modal-button/index.js index 14e1414c85..b0925fb0d2 100644 --- a/pkg/webui/components/qr-modal-button/index.js +++ b/pkg/webui/components/qr-modal-button/index.js @@ -39,34 +39,36 @@ const QrScanDoc = ( const m = defineMessages({ scanContinue: 'Please scan the QR code to continue. {qrScanDoc}', apply: 'Apply', - fleetToken: 'Fleet token', - addFleet: 'Add Fleet', + fleetToken: 'Fleet owner token', + addFleet: 'Add to Fleet', + addToFleetTooltip: + 'You are registering a Managed gateway. If you want to add it to an existing fleet, click here.', }) const QRModalButton = props => { const { message, onApprove, onCancel, onRead, qrData, setQrData, invalidMessage } = props - const [isAddFleet, setIsAddFleet] = React.useState(undefined) - const [fleetToken, setFleetToken] = React.useState('') + const [isAddToFleet, setIsAddToFleet] = React.useState(undefined) + const [fleetOwnerToken, setFleetOwnerToken] = React.useState('') - const handleAddFleet = useCallback(() => { - setIsAddFleet(true) + const handleAddToFleet = useCallback(() => { + setIsAddToFleet(true) }, []) const handleRemoveFleet = useCallback(() => { - setIsAddFleet(false) - setFleetToken('') + setIsAddToFleet(false) + setFleetOwnerToken('') setQrData({ ...qrData, - gateway: { ...qrData.gateway, _fleet_token: undefined }, + gateway: { ...qrData.gateway, _fleet_owner_token: undefined }, }) }, [qrData, setQrData]) const handleFleetTokenChange = useCallback( value => { - setFleetToken(value) + setFleetOwnerToken(value) setQrData({ ...qrData, - gateway: { ...qrData.gateway, _fleet_token: value }, + gateway: { ...qrData.gateway, _fleet_owner_token: value }, }) }, [qrData, setQrData], @@ -87,7 +89,7 @@ const QRModalButton = props => { {qrData.gateway.is_managed && ( <> - {isAddFleet ? ( + {isAddToFleet ? (
@@ -107,7 +109,7 @@ const QRModalButton = props => { sensitive className="w-full" inputWidth="full" - value={fleetToken} + value={fleetOwnerToken} onChange={handleFleetTokenChange} /> { tertiary message={m.addFleet} icon={IconPlus} - onClick={handleAddFleet} + onClick={handleAddToFleet} className="mt-cs-xs" + tooltip={m.addToFleetTooltip} + tooltipPlacement="bottom" /> )} @@ -179,7 +183,6 @@ QRModalButton.propTypes = { data: PropTypes.arrayOf(PropTypes.shape()), gateway: PropTypes.shape({ is_managed: PropTypes.bool, - _fleet_token: PropTypes.string, }), }), setQrData: PropTypes.func.isRequired, diff --git a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js index 55e1001093..a72220a248 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js @@ -76,10 +76,10 @@ const GatewayClaimFormSection = () => { authenticated_identifiers: { ...values.authenticated_identifiers, authentication_code: - value === 'fleet' - ? btoa(values._fleet_token) - : value === 'gateway' - ? btoa(values._owner_token) + value === 'fleet' && values._fleet_owner_token + ? btoa(values._fleet_owner_token) + : value === 'gateway' && values._gtw_owner_token + ? btoa(values._gtw_owner_token) : '', }, })) diff --git a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js index f391acb75a..f0a7c77c41 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/index.js @@ -37,6 +37,7 @@ const m = defineMessages({ gatewayGuide: 'Gateway registration help', invalidQRCode: 'Invalid QR code data. Please note that only TTIGPRO1 Gateway Identification QR Code can be scanned. Some gateways have unrelated QR codes printed on them that cannot be used.', + gatewayOwnerToken: 'Gateway owner token', }) const qrDataInitialState = { @@ -67,15 +68,15 @@ const GatewayQRScanSection = () => { }, authenticated_identifiers: { gateway_eui: gateway.gateway_eui, - authentication_code: gateway._fleet_token - ? btoa(gateway._fleet_token) + authentication_code: gateway._fleet_owner_token + ? btoa(gateway._fleet_owner_token) : gateway.owner_token ? btoa(gateway.owner_token) : '', }, - _owner_token: gateway.owner_token ?? '', - _fleet_token: gateway._fleet_token ?? '', - _isFleet: Boolean(gateway._fleet_token), + _gtw_owner_token: gateway.owner_token ?? '', + _fleet_owner_token: gateway._fleet_owner_token ?? '', + _isFleet: Boolean(gateway._fleet_owner_token), })) setQrData({ ...qrData, approved: true }) @@ -98,18 +99,18 @@ const GatewayQRScanSection = () => { { header: sharedMessages.qrCodeData, items: [ - { - key: sharedMessages.ownerToken, - value: gateway.owner_token, - type: 'code', - sensitive: true, - }, { key: sharedMessages.gatewayEUI, value: gateway.gateway_eui, type: 'byte', sensitive: false, }, + { + key: m.gatewayOwnerToken, + value: gateway.owner_token, + type: 'code', + sensitive: true, + }, ], }, ] From aacb9d2b5c17990d8f52825c4b44f8a2fd9ccb9d Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 13:57:21 +0100 Subject: [PATCH 08/23] console: Fix error message --- pkg/webui/lib/errors/utils.js | 5 +---- pkg/webui/locales/en.json | 11 ++++++++--- pkg/webui/locales/ja.json | 5 +++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pkg/webui/lib/errors/utils.js b/pkg/webui/lib/errors/utils.js index a292fad5c1..4fd52a8e30 100644 --- a/pkg/webui/lib/errors/utils.js +++ b/pkg/webui/lib/errors/utils.js @@ -602,7 +602,7 @@ export const getClaimGatewayErrorMessage = error => { const m = defineMessages({ notFound: "Gateway doesn't exist. Please confirm that the gateway EUI is correct.", subscriptionNotActive: - 'There is no gateway subscription attached or active. Please get a Gateway Subscription or activate your subscription following the steps in the documentation and try again.', + 'There is no gateway subscription attached or active. Please get a Gateway Subscription or activate your subscription following the steps in the documentation. If this gateway is part of a fleet, you should use a Fleet Owner Token during the registration process.', activationCodeExpired: 'The activation code has expired. To reactivate it, extend your Gateway Subscription.', permissionDenied: 'The owner token is invalid.', @@ -618,9 +618,6 @@ export const getClaimGatewayErrorMessage = error => { if (backendErrorMessage.includes('gateway subscription not attached and active')) { return m.subscriptionNotActive } - if (backendErrorMessage.includes('activation code expired')) { - return m.activationCodeExpired - } return undefined case 7: // PERMISSION_DENIED return m.permissionDenied diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index de92045201..24922584d0 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -62,8 +62,9 @@ "components.progress-bar.index.percentage": "{percentage, number, percent} finished", "components.qr-modal-button.index.scanContinue": "Please scan the QR code to continue. {qrScanDoc}", "components.qr-modal-button.index.apply": "Apply", - "components.qr-modal-button.index.fleetToken": "Fleet token", - "components.qr-modal-button.index.addFleet": "Add Fleet", + "components.qr-modal-button.index.fleetToken": "Fleet owner token", + "components.qr-modal-button.index.addFleet": "Add to Fleet", + "components.qr-modal-button.index.addToFleetTooltip": "You are registering a Managed gateway. If you want to add it to an existing fleet, click here.", "components.qr.input.capture.index.uploadImage": "Upload a Photo", "components.qr.input.capture.index.qrCodeNotFound": "QR code not found", "components.qr.input.video.index.fetchingCamera": "Waiting for camera…", @@ -101,6 +102,9 @@ "console.components.application-general-settings-form.index.adminContactDescription": "Administrative contact information for this application. Typically used to indicate who to contact with administrative questions about the application.", "console.components.application-general-settings-form.index.techContactDescription": "Technical contact information for this application. Typically used to indicate who to contact with technical/security questions about the application.", "console.components.application-map-panel.index.deviceLocations": "Device locations", + "console.components.claim-deprecation-modal.index.modalTitle": "The Claim App has been removed", + "console.components.claim-deprecation-modal.index.modalMessage": "The Claim App and in-cluster claiming has been removed in version 3.25.2. You can use the regular device registration flow in the Console, which includes claiming if applicable, or use the claiming feature of the CLI. For more information, please refer to the Device Claiming documentation.", + "console.components.claim-deprecation-modal.index.buttonMessage": "Close and use Console", "console.components.default-routing-policy-form.index.doNotUseADefaultPolicy": "Do not use a default routing policy for this network", "console.components.device-import-form.index.file": "File", "console.components.device-import-form.index.formatInfo": "Format information", @@ -627,6 +631,7 @@ "console.containers.gateway-onboarding-form.qr-scan-section.index.hasGatewayQR": "Does your gateway have a LoRaWAN® Gateway Identification QR Code? Scan it to speed up onboarding.", "console.containers.gateway-onboarding-form.qr-scan-section.index.gatewayGuide": "Gateway registration help", "console.containers.gateway-onboarding-form.qr-scan-section.index.invalidQRCode": "Invalid QR code data. Please note that only TTIGPRO1 Gateway Identification QR Code can be scanned. Some gateways have unrelated QR codes printed on them that cannot be used.", + "console.containers.gateway-onboarding-form.qr-scan-section.index.gatewayOwnerToken": "Gateway owner token", "console.containers.gateway-overview-header.index.addBookmarkFail": "There was an error and the gateway could not be bookmarked", "console.containers.gateway-overview-header.index.duplicateGateway": "Duplicate gateway", "console.containers.gateway-overview-header.index.removeBookmarkFail": "There was an error and the gateway could not be removed from bookmarks", @@ -1086,7 +1091,7 @@ "lib.errors.status-code-messages.503": "Service unavailable", "lib.errors.status-code-messages.504": "Gateway timeout", "lib.errors.utils.notFound": "Gateway doesn't exist. Please confirm that the gateway EUI is correct.", - "lib.errors.utils.subscriptionNotActive": "There is no gateway subscription attached or active. Please get a Gateway Subscription or activate your subscription following the steps in the documentation and try again.", + "lib.errors.utils.subscriptionNotActive": "There is no gateway subscription attached or active. Please get a Gateway Subscription or activate your subscription following the steps in the documentation. If this gateway is part of a fleet, you should use a Fleet Owner Token during the registration process.", "lib.errors.utils.activationCodeExpired": "The activation code has expired. To reactivate it, extend your Gateway Subscription.", "lib.errors.utils.permissionDenied": "The owner token is invalid.", "lib.field-description-messages.idLocation": "Enter a value using lowercase letters, numbers, and dashes. You can choose this freely.", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index fd3b94dcaf..7a460171af 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -64,6 +64,7 @@ "components.qr-modal-button.index.apply": "適用", "components.qr-modal-button.index.fleetToken": "", "components.qr-modal-button.index.addFleet": "", + "components.qr-modal-button.index.addToFleetTooltip": "", "components.qr.input.capture.index.uploadImage": "写真をアップロード", "components.qr.input.capture.index.qrCodeNotFound": "QRコードが見つかりません", "components.qr.input.video.index.fetchingCamera": "", @@ -101,6 +102,9 @@ "console.components.application-general-settings-form.index.adminContactDescription": "このアプリケーションの管理者の連絡先情報。通常、アプリケーションに関する管理上の問い合わせ先を示すために使用されます。", "console.components.application-general-settings-form.index.techContactDescription": "このアプリケーションの技術的な連絡先情報。通常、アプリケーションに関する技術/セキュリティの質問の連絡先を示すために使用されます。", "console.components.application-map-panel.index.deviceLocations": "", + "console.components.claim-deprecation-modal.index.modalTitle": "", + "console.components.claim-deprecation-modal.index.modalMessage": "", + "console.components.claim-deprecation-modal.index.buttonMessage": "", "console.components.default-routing-policy-form.index.doNotUseADefaultPolicy": "このネットワークでは、デフォルトのルーティングポリシーを使用しないでください", "console.components.device-import-form.index.file": "", "console.components.device-import-form.index.formatInfo": "", @@ -627,6 +631,7 @@ "console.containers.gateway-onboarding-form.qr-scan-section.index.hasGatewayQR": "", "console.containers.gateway-onboarding-form.qr-scan-section.index.gatewayGuide": "", "console.containers.gateway-onboarding-form.qr-scan-section.index.invalidQRCode": "", + "console.containers.gateway-onboarding-form.qr-scan-section.index.gatewayOwnerToken": "", "console.containers.gateway-overview-header.index.addBookmarkFail": "", "console.containers.gateway-overview-header.index.duplicateGateway": "", "console.containers.gateway-overview-header.index.removeBookmarkFail": "", From 4c541d08139b6d9d1a8157cadfc1faa44a90d3af Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 14:03:32 +0100 Subject: [PATCH 09/23] console: Fix e2e tests --- cypress/e2e/console/gateways/create.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/console/gateways/create.spec.js b/cypress/e2e/console/gateways/create.spec.js index b1fd349c80..b00762cb6c 100644 --- a/cypress/e2e/console/gateways/create.spec.js +++ b/cypress/e2e/console/gateways/create.spec.js @@ -190,7 +190,7 @@ describe('Gateway create', () => { cy.findByLabelText('Gateway EUI').type(gateway.eui) cy.findByRole('button', { name: 'Confirm' }).click() cy.findByTestId('notification').should('be.visible') - cy.findByLabelText('Owner token').type('12345') + cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345') cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`) cy.findByText('Frequency plan') .parents('div[data-test-id="form-field"]') @@ -239,7 +239,7 @@ describe('Gateway create', () => { cy.findByTestId('notification').should('be.visible') cy.findByLabelText('Frequency plan').selectOption(gateway.frequency_plan) cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`) - cy.findByLabelText('Owner token').type('12345') + cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345') cy.findByRole('button', { name: 'Claim gateway' }).click() cy.wait('@claim-request').its('request.body').should('deep.equal', expectedRequest) cy.findByTestId('error-notification').should('not.exist') From 987e80e0521edf0b37e9ee0cc5a199efb9786c07 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 14:35:41 +0100 Subject: [PATCH 10/23] console: Add correct messages --- pkg/webui/components/qr-modal-button/index.js | 14 ++++++++++++-- .../gateway-claim-form-section/index.js | 10 +++++++++- pkg/webui/lib/shared-messages.js | 4 ++-- pkg/webui/locales/en.json | 4 ++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/pkg/webui/components/qr-modal-button/index.js b/pkg/webui/components/qr-modal-button/index.js index b0925fb0d2..80d1c0e406 100644 --- a/pkg/webui/components/qr-modal-button/index.js +++ b/pkg/webui/components/qr-modal-button/index.js @@ -26,10 +26,13 @@ import ErrorMessage from '@ttn-lw/lib/components/error-message' import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' +import { selectPluginTTSCloud } from '@ttn-lw/lib/selectors/env' import DataSheet from '../data-sheet' import QR from '../qr' +const smUrl = selectPluginTTSCloud().subscription_management_url + const QrScanDoc = ( Having trouble? @@ -100,7 +103,14 @@ const QRModalButton = props => { />
( + + {val} + + ), + }} className="mb-cs-xs mt-cs-xxs c-text-neutral-light fs-s" component="div" /> @@ -113,7 +123,7 @@ const QRModalButton = props => { onChange={handleFleetTokenChange} /> diff --git a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js index a72220a248..a15f80c844 100644 --- a/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js +++ b/pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js @@ -31,12 +31,13 @@ import Message from '@ttn-lw/lib/components/message' import { GsFrequencyPlansSelect as FrequencyPlansSelect } from '@console/containers/freq-plans-select' -import { selectGsConfig } from '@ttn-lw/lib/selectors/env' +import { selectGsConfig, selectPluginTTSCloud } from '@ttn-lw/lib/selectors/env' import sharedMessages from '@ttn-lw/lib/shared-messages' import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids' import getHostFromUrl from '@ttn-lw/lib/host-from-url' const { enabled: gsEnabled, base_url: gsBaseURL } = selectGsConfig() +const smUrl = selectPluginTTSCloud().subscription_management_url const m = defineMessages({ claimWarning: @@ -135,6 +136,13 @@ const GatewayClaimFormSection = () => { tooltipId={tooltipIds.CLAIM_AUTH_CODE} component={Input} description={activeOwnerTokenType === 'fleet' ? sharedMessages.fleetTokenInfo : undefined} + descriptionValues={{ + Link: val => ( + + {val} + + ), + }} encode={btoa} decode={atob} sensitive diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index 7b96ea12b0..47e3e3d82d 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -278,8 +278,8 @@ export default defineMessages({ fCnt: 'FCnt', fetching: 'Fetching data…', firmwareVersion: 'Firmware version', - fleetInfo: 'Adding a gateway to a fleet will blablabla.', - fleetTokenInfo: 'Your fleet token is available in your account.', + fleetInfo: 'Adding a gateway to a Fleet will claim the gateway using an available Slot.', + fleetTokenInfo: 'Your fleet token is available in your account.', format: 'Format', fpNotFoundError: 'The LoRaWAN version {lorawanVersion} does not support the {freqPlan} frequency plan. Please choose a different MAC version or frequency plan.', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 24922584d0..c274b86b4f 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -1446,8 +1446,8 @@ "lib.shared-messages.fCnt": "FCnt", "lib.shared-messages.fetching": "Fetching data…", "lib.shared-messages.firmwareVersion": "Firmware version", - "lib.shared-messages.fleetInfo": "Adding a gateway to a fleet will blablabla.", - "lib.shared-messages.fleetTokenInfo": "Your fleet token is available in your account.", + "lib.shared-messages.fleetInfo": "Adding a gateway to a Fleet will claim the gateway using an available Slot.", + "lib.shared-messages.fleetTokenInfo": "Your fleet token is available in your account.", "lib.shared-messages.format": "Format", "lib.shared-messages.fpNotFoundError": "The LoRaWAN version {lorawanVersion} does not support the {freqPlan} frequency plan. Please choose a different MAC version or frequency plan.", "lib.shared-messages.fPort": "FPort", From af400c113bfc0fa9faad59d7313156e3234f85c4 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Dec 2025 14:56:34 +0100 Subject: [PATCH 11/23] console: Separate tti only parts and gateway fleet only parts --- pkg/webui/components/qr-modal-button/index.js | 94 +------------- .../gateway-claim-form-section/index.js | 14 ++- .../qr-scan-section/fleets-scan.tti.js | 116 ++++++++++++++++++ .../qr-scan-section/index.js | 10 +- pkg/webui/locales/en.json | 6 +- pkg/webui/locales/ja.json | 6 +- 6 files changed, 149 insertions(+), 97 deletions(-) create mode 100644 pkg/webui/console/containers/gateway-onboarding-form/qr-scan-section/fleets-scan.tti.js diff --git a/pkg/webui/components/qr-modal-button/index.js b/pkg/webui/components/qr-modal-button/index.js index 80d1c0e406..da31b1f249 100644 --- a/pkg/webui/components/qr-modal-button/index.js +++ b/pkg/webui/components/qr-modal-button/index.js @@ -15,24 +15,19 @@ import React, { useCallback } from 'react' import { defineMessages } from 'react-intl' -import { IconCamera, IconPlus } from '@ttn-lw/components/icon' +import { IconCamera } from '@ttn-lw/components/icon' import Link from '@ttn-lw/components/link' import ModalButton from '@ttn-lw/components/button/modal-button' -import Button from '@ttn-lw/components/button' -import Input from '@ttn-lw/components/input' import Message from '@ttn-lw/lib/components/message' import ErrorMessage from '@ttn-lw/lib/components/error-message' import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' -import { selectPluginTTSCloud } from '@ttn-lw/lib/selectors/env' import DataSheet from '../data-sheet' import QR from '../qr' -const smUrl = selectPluginTTSCloud().subscription_management_url - const QrScanDoc = ( Having trouble? @@ -42,40 +37,10 @@ const QrScanDoc = ( const m = defineMessages({ scanContinue: 'Please scan the QR code to continue. {qrScanDoc}', apply: 'Apply', - fleetToken: 'Fleet owner token', - addFleet: 'Add to Fleet', - addToFleetTooltip: - 'You are registering a Managed gateway. If you want to add it to an existing fleet, click here.', }) const QRModalButton = props => { - const { message, onApprove, onCancel, onRead, qrData, setQrData, invalidMessage } = props - const [isAddToFleet, setIsAddToFleet] = React.useState(undefined) - const [fleetOwnerToken, setFleetOwnerToken] = React.useState('') - - const handleAddToFleet = useCallback(() => { - setIsAddToFleet(true) - }, []) - - const handleRemoveFleet = useCallback(() => { - setIsAddToFleet(false) - setFleetOwnerToken('') - setQrData({ - ...qrData, - gateway: { ...qrData.gateway, _fleet_owner_token: undefined }, - }) - }, [qrData, setQrData]) - - const handleFleetTokenChange = useCallback( - value => { - setFleetOwnerToken(value) - setQrData({ - ...qrData, - gateway: { ...qrData.gateway, _fleet_owner_token: value }, - }) - }, - [qrData, setQrData], - ) + const { message, onApprove, onCancel, onRead, qrData, invalidMessage, modalDataChildren } = props const handleRead = useCallback( val => { @@ -90,57 +55,7 @@ const QRModalButton = props => { qrData.valid ? ( <> - {qrData.gateway.is_managed && ( - <> - {isAddToFleet ? ( -
-
- -
- ( - - {val} - - ), - }} - className="mb-cs-xs mt-cs-xxs c-text-neutral-light fs-s" - component="div" - /> - - -
- ) : ( -
+ ( + + {val} + + ), + }} + className="mb-cs-xs mt-cs-xxs c-text-neutral-light fs-s" + component="div" + /> + + +
+ ) : ( +