Skip to content

Conversation

@cryptodev-2s
Copy link
Contributor

@cryptodev-2s cryptodev-2s commented Jan 14, 2026

Explanation

This PR introduces a new @metamask/connectivity-controller package that provides a centralized way to track and manage device internet connectivity status across MetaMask clients (extension and mobile).

What is the current state of things and why does it need to change?

  • Have a single source of truth for connectivity state
  • Share connectivity status across controllers and UI components
  • Test connectivity-related functionality in isolation
  • Maintain consistent behavior across platforms

What is the solution your changes offer and how does it work?

The ConnectivityController is a platform-agnostic controller that:

  • Stores the current connectivity status (online or offline) in its state
  • Uses a service injection pattern where platform-specific ConnectivityService implementations are injected at construction time
  • Subscribes to connectivity changes from the service and automatically updates its state
  • Emits stateChange events when connectivity status changes, allowing other controllers and UI components to react accordingly

The controller is designed to work with different service implementations:

  • Mobile: NetInfoConnectivityService using @react-native-community/netinfo
  • Extension (same context): BrowserConnectivityService using browser APIs

Are there any changes whose purpose might not be obvious to those unfamiliar with the domain?

  • The connectivityStatus field in state is prefixed with "connectivity" to avoid conflicts when state is flattened in Redux
  • The state is configured with persist: false since connectivity status is ephemeral and should not be restored from storage

References https://consensyssoftware.atlassian.net/browse/WPC-210

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

New package: @metamask/connectivity-controller

  • Adds ConnectivityController (extends BaseController) to store device connectivity status in connectivityStatus using an injected ConnectivityService; subscribes to onConnectivityChange and emits ConnectivityController:stateChange
  • Exposes types (ConnectivityStatus, ConnectivityService, controller messenger/actions/events) via src/index.ts; defaults to online and state is not persisted
  • Comprehensive tests in src/ConnectivityController.test.ts; package setup includes package.json, Jest/TypeDoc configs, LICENSE, README, and CHANGELOG

Monorepo integrations

  • Updates README.md (package list and dependency graph), .github/CODEOWNERS, teams.json, tsconfig.json, tsconfig.build.json, and yarn.lock to register the new package

Written by Cursor Bugbot for commit 9e00b40. This will update automatically on new commits. Configure here.

@cryptodev-2s cryptodev-2s force-pushed the feature/add-connectivity-controller branch from b14162d to daa5333 Compare January 14, 2026 12:14
@cryptodev-2s cryptodev-2s force-pushed the feature/add-connectivity-controller branch from daa5333 to 5e51275 Compare January 14, 2026 12:24
@cryptodev-2s cryptodev-2s marked this pull request as ready for review January 14, 2026 13:46
@cryptodev-2s cryptodev-2s requested a review from a team as a code owner January 14, 2026 13:46
@cryptodev-2s
Copy link
Contributor Author

@metamaskbot publish-preview

draftState.connectivityStatus = status;
});
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller doesn't clean up connectivity service subscription on destroy

Medium Severity

The ConnectivityController subscribes to connectivityService.onConnectivityChange() in the constructor but neither stores a reference to the service nor overrides destroy() to clean up. The BaseController.destroy() documentation states subclasses "should be extended...to clean up any additional connections or events." Other controllers like TokenListController and CurrencyRateController follow this pattern. Without cleanup, if the controller is destroyed while the service continues running, the service retains a reference to the callback (which captures this), potentially causing memory leaks and unintended state updates on a destroyed controller.

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. We haven't been consistent about implementing destroy. But maybe we should do this?

If the ConnectivityService used the messenger then we could call unsubscribe. Otherwise, we would need ConnectivityService to implement EventEmitter so that it could call removeEventListener in its destroy.

Maybe ConnectivityService should use the messenger? Curious to know your thoughts.

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.0.0-preview-9e00b40b",
  "@metamask-previews/accounts-controller": "35.0.1-preview-9e00b40b",
  "@metamask-previews/address-book-controller": "7.0.1-preview-9e00b40b",
  "@metamask-previews/analytics-controller": "1.0.0-preview-9e00b40b",
  "@metamask-previews/announcement-controller": "8.0.0-preview-9e00b40b",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-9e00b40b",
  "@metamask-previews/approval-controller": "8.0.0-preview-9e00b40b",
  "@metamask-previews/assets-controller": "0.0.0-preview-9e00b40b",
  "@metamask-previews/assets-controllers": "95.1.0-preview-9e00b40b",
  "@metamask-previews/base-controller": "9.0.0-preview-9e00b40b",
  "@metamask-previews/bridge-controller": "64.4.1-preview-9e00b40b",
  "@metamask-previews/bridge-status-controller": "64.4.2-preview-9e00b40b",
  "@metamask-previews/build-utils": "3.0.4-preview-9e00b40b",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-9e00b40b",
  "@metamask-previews/claims-controller": "0.4.1-preview-9e00b40b",
  "@metamask-previews/composable-controller": "12.0.0-preview-9e00b40b",
  "@metamask-previews/connectivity-controller": "0.0.0-preview-9e00b40b",
  "@metamask-previews/controller-utils": "11.18.0-preview-9e00b40b",
  "@metamask-previews/core-backend": "5.0.0-preview-9e00b40b",
  "@metamask-previews/delegation-controller": "2.0.0-preview-9e00b40b",
  "@metamask-previews/earn-controller": "11.0.0-preview-9e00b40b",
  "@metamask-previews/eip-5792-middleware": "2.1.0-preview-9e00b40b",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-9e00b40b",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-9e00b40b",
  "@metamask-previews/ens-controller": "19.0.1-preview-9e00b40b",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-9e00b40b",
  "@metamask-previews/eth-block-tracker": "15.0.0-preview-9e00b40b",
  "@metamask-previews/eth-json-rpc-middleware": "22.0.1-preview-9e00b40b",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-9e00b40b",
  "@metamask-previews/foundryup": "1.0.1-preview-9e00b40b",
  "@metamask-previews/gas-fee-controller": "26.0.1-preview-9e00b40b",
  "@metamask-previews/gator-permissions-controller": "0.8.0-preview-9e00b40b",
  "@metamask-previews/json-rpc-engine": "10.2.0-preview-9e00b40b",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-9e00b40b",
  "@metamask-previews/keyring-controller": "25.0.0-preview-9e00b40b",
  "@metamask-previews/logging-controller": "7.0.1-preview-9e00b40b",
  "@metamask-previews/message-manager": "14.1.0-preview-9e00b40b",
  "@metamask-previews/messenger": "0.3.0-preview-9e00b40b",
  "@metamask-previews/multichain-account-service": "5.0.0-preview-9e00b40b",
  "@metamask-previews/multichain-api-middleware": "1.2.5-preview-9e00b40b",
  "@metamask-previews/multichain-network-controller": "3.0.1-preview-9e00b40b",
  "@metamask-previews/multichain-transactions-controller": "7.0.0-preview-9e00b40b",
  "@metamask-previews/name-controller": "9.0.0-preview-9e00b40b",
  "@metamask-previews/network-controller": "28.0.0-preview-9e00b40b",
  "@metamask-previews/network-enablement-controller": "4.0.0-preview-9e00b40b",
  "@metamask-previews/notification-services-controller": "21.0.0-preview-9e00b40b",
  "@metamask-previews/permission-controller": "12.2.0-preview-9e00b40b",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-9e00b40b",
  "@metamask-previews/phishing-controller": "16.1.0-preview-9e00b40b",
  "@metamask-previews/polling-controller": "16.0.1-preview-9e00b40b",
  "@metamask-previews/preferences-controller": "22.0.0-preview-9e00b40b",
  "@metamask-previews/profile-metrics-controller": "2.0.0-preview-9e00b40b",
  "@metamask-previews/profile-sync-controller": "27.0.0-preview-9e00b40b",
  "@metamask-previews/ramps-controller": "3.0.0-preview-9e00b40b",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-9e00b40b",
  "@metamask-previews/remote-feature-flag-controller": "4.0.0-preview-9e00b40b",
  "@metamask-previews/sample-controllers": "4.0.1-preview-9e00b40b",
  "@metamask-previews/seedless-onboarding-controller": "7.1.0-preview-9e00b40b",
  "@metamask-previews/selected-network-controller": "26.0.1-preview-9e00b40b",
  "@metamask-previews/shield-controller": "4.1.0-preview-9e00b40b",
  "@metamask-previews/signature-controller": "38.0.1-preview-9e00b40b",
  "@metamask-previews/storage-service": "0.0.1-preview-9e00b40b",
  "@metamask-previews/subscription-controller": "5.4.0-preview-9e00b40b",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-9e00b40b",
  "@metamask-previews/transaction-controller": "62.9.1-preview-9e00b40b",
  "@metamask-previews/transaction-pay-controller": "11.0.1-preview-9e00b40b",
  "@metamask-previews/user-operation-controller": "41.0.1-preview-9e00b40b"
}

export type ConnectivityControllerState = {
/**
* The current device connectivity status.
* Named with 'connectivity' prefix to avoid conflicts when state is flattened in Redux.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

Comment on lines +119 to +124
*
* Platform implementations:
* - Mobile: Use `NetInfoConnectivityService` with `@react-native-community/netinfo`
* - Extension (same context): Use `BrowserConnectivityService`
* - Extension (cross-context): Use `PassiveConnectivityService` and call
* `setStatus()` from the UI context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include this here? It seems that we would need to update this documentation if the implementation in the client changes.

Suggested change
*
* Platform implementations:
* - Mobile: Use `NetInfoConnectivityService` with `@react-native-community/netinfo`
* - Extension (same context): Use `BrowserConnectivityService`
* - Extension (cross-context): Use `PassiveConnectivityService` and call
* `setStatus()` from the UI context

Comment on lines +140 to +146
* **Platform implementations:**
*
* - **Mobile:** Inject `NetInfoConnectivityService` using `@react-native-community/netinfo`
* - **Extension:** Inject `PassiveConnectivityService` in the background.
* Status is updated via the `setDeviceConnectivityStatus` API, which is called from:
* - MV3: Offscreen document (where browser events work reliably)
* - MV2: Background page (where browser events work directly)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above — do we need to include this in the documentation?

Suggested change
* **Platform implementations:**
*
* - **Mobile:** Inject `NetInfoConnectivityService` using `@react-native-community/netinfo`
* - **Extension:** Inject `PassiveConnectivityService` in the background.
* Status is updated via the `setDeviceConnectivityStatus` API, which is called from:
* - MV3: Offscreen document (where browser events work reliably)
* - MV2: Background page (where browser events work directly)

draftState.connectivityStatus = status;
});
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. We haven't been consistent about implementing destroy. But maybe we should do this?

If the ConnectivityService used the messenger then we could call unsubscribe. Otherwise, we would need ConnectivityService to implement EventEmitter so that it could call removeEventListener in its destroy.

Maybe ConnectivityService should use the messenger? Curious to know your thoughts.

* Connectivity status constants.
* Used to represent whether the device has internet connectivity.
*/
export const ConnectivityStatus = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about calling this CONNECTIVITY_STATUSES? We don't have formal guidelines on these "quasi-enums", but I recently found this guide and have found it to be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants