diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2c6347e84..d0927539ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ For details about compatibility between different releases, see the **Commitment
### Changed
+- Improve error messages when claiming a managed gateway.
+
### Deprecated
### Removed
diff --git a/pkg/webui/console/containers/gateway-onboarding-form/index.js b/pkg/webui/console/containers/gateway-onboarding-form/index.js
index f990a82b79..e9a70918a2 100644
--- a/pkg/webui/console/containers/gateway-onboarding-form/index.js
+++ b/pkg/webui/console/containers/gateway-onboarding-form/index.js
@@ -17,12 +17,14 @@ import { useSelector, useDispatch } from 'react-redux'
import { merge } from 'lodash'
import Form from '@ttn-lw/components/form'
+import Link from '@ttn-lw/components/link'
import GatewayApiKeysModal from '@console/components/gateway-api-keys-modal'
import { composeDataUri, downloadDataUriAsFile } from '@ttn-lw/lib/data-uri'
import PropTypes from '@ttn-lw/lib/prop-types'
import attachPromise from '@ttn-lw/lib/store/actions/attach-promise'
+import { getClaimGatewayErrorMessage } from '@ttn-lw/lib/errors/utils'
import { createGateway, claimGateway, updateGateway } from '@console/store/actions/gateways'
import { createGatewayApiKey } from '@console/store/actions/api-keys'
@@ -141,7 +143,27 @@ const GatewayOnboardingForm = props => {
onSuccess(cleanValues.target_gateway_id, inputMethod === 'managed')
} catch (error) {
- setError(error)
+ let message = getClaimGatewayErrorMessage(error)
+
+ if (message) {
+ message = {
+ ...message,
+ values: {
+ link: content => (
+
+ {content}
+
+ ),
+ },
+ }
+ setError(message)
+ } else {
+ // Fallback for unexpected/unhandled errors
+ setError(error)
+ }
}
},
[dispatch, onSuccess, userId],
diff --git a/pkg/webui/lib/errors/utils.js b/pkg/webui/lib/errors/utils.js
index 5854b35efc..8032a18d19 100644
--- a/pkg/webui/lib/errors/utils.js
+++ b/pkg/webui/lib/errors/utils.js
@@ -14,6 +14,7 @@
import * as Sentry from '@sentry/react'
import { isPlainObject, isObject } from 'lodash'
+import { defineMessages } from 'react-intl'
import { error as errorLog, warn } from '@ttn-lw/lib/log'
import interpolate from '@ttn-lw/lib/interpolate'
@@ -588,3 +589,40 @@ export const ingestError = (error, extras = {}, tags = {}) => {
})
}
}
+
+/**
+ * Maps claim-related backend errors to appropriate user messages.
+ *
+ * @param {object} error - The error object.
+ * @returns {object|undefined} - The corresponding error message, or undefined if no match.
+ */
+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.',
+ activationCodeExpired:
+ 'The activation code has expired. To reactivate it, extend your Gateway Subscription.',
+ permissionDenied: 'The owner token is invalid.',
+ })
+
+ const rootCause = getBackendErrorRootCause(error)
+ const errorCode = rootCause?.code
+ const backendErrorMessage = rootCause?.message_format
+ switch (errorCode) {
+ case 5: // NOT_FOUND
+ return m.notFound
+ case 9: // FAILED_PRECONDITION
+ 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
+ default:
+ return undefined
+ }
+}
diff --git a/pkg/webui/lib/prop-types.js b/pkg/webui/lib/prop-types.js
index acdc1575d0..25341d0bc5 100644
--- a/pkg/webui/lib/prop-types.js
+++ b/pkg/webui/lib/prop-types.js
@@ -30,7 +30,7 @@ PropTypes.formatters = PropTypes.shape({
PropTypes.message = PropTypes.oneOfType([
PropTypes.shape({
id: PropTypes.string.isRequired,
- value: PropTypes.shape({}),
+ values: PropTypes.shape({}),
defaultMessage: PropTypes.string,
}),
PropTypes.string,
diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json
index 21c47bc6ec..218213ba45 100644
--- a/pkg/webui/locales/en.json
+++ b/pkg/webui/locales/en.json
@@ -1094,6 +1094,10 @@
"lib.errors.status-code-messages.501": "Not implemented",
"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.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.",
"lib.field-description-messages.freqPlanDescription": "A frequency plan defines data rates that your end device or gateway is setup to use. It is important that gateways and end devices within reach use the same frequency plan to be able to communicate.",
"lib.field-description-messages.freqPlanLocation": "Your end device or gateway manufacturer should provide information about the applicable frequency plan for a particular device. In some cases they are printed on the device itself but they should always be in the hardware manual or data sheet.",
diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json
index fbfcd56ede..5aac450cd2 100644
--- a/pkg/webui/locales/ja.json
+++ b/pkg/webui/locales/ja.json
@@ -1094,6 +1094,10 @@
"lib.errors.status-code-messages.501": "",
"lib.errors.status-code-messages.503": "",
"lib.errors.status-code-messages.504": "",
+ "lib.errors.utils.notFound": "",
+ "lib.errors.utils.subscriptionNotActive": "",
+ "lib.errors.utils.activationCodeExpired": "",
+ "lib.errors.utils.permissionDenied": "",
"lib.field-description-messages.idLocation": "",
"lib.field-description-messages.freqPlanDescription": "周波数プランは、エンドデバイスやゲートウェイが使用するように設定されたデータレートを定義します。ゲートウェイとエンドデバイスが通信できるようにするためには、同じ周波数プランを使用することが重要です",
"lib.field-description-messages.freqPlanLocation": "エンドデバイスやゲートウェイのメーカーは、特定のデバイスに適用される周波数プランに関する情報を提供するはずです。場合によっては、デバイス自体に印刷されていることもありますが、常にハードウェアマニュアルまたはデータシートに記載されているはずです",