diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index bd017c5bf6cd..34441fea767d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4533,6 +4533,9 @@ "onboardingMetametricCheckboxDescriptionOne": { "message": "We’ll collect basic product usage data like general location, clicks, and views. No other information will be stored." }, + "onboardingMetametricCheckboxDescriptionOneUpdated": { + "message": "We’ll collect basic product usage data. We may associate this information with on-chain data." + }, "onboardingMetametricCheckboxDescriptionTwo": { "message": "We’ll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features)." }, @@ -5051,6 +5054,9 @@ "message": "+ $1 more", "description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items" }, + "pna25BannerTitle": { + "message": "We’re updating MetaMetrics. We'll associate some in-app activity to on-chain data to better understand usage. You can opt out via Settings." + }, "popularNetworkAddToolTip": { "message": "Some of these networks rely on third parties. The connections may be less reliable or enable third-parties to track activity.", "description": "Learn more link" diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index bd017c5bf6cd..34441fea767d 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -4533,6 +4533,9 @@ "onboardingMetametricCheckboxDescriptionOne": { "message": "We’ll collect basic product usage data like general location, clicks, and views. No other information will be stored." }, + "onboardingMetametricCheckboxDescriptionOneUpdated": { + "message": "We’ll collect basic product usage data. We may associate this information with on-chain data." + }, "onboardingMetametricCheckboxDescriptionTwo": { "message": "We’ll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features)." }, @@ -5051,6 +5054,9 @@ "message": "+ $1 more", "description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items" }, + "pna25BannerTitle": { + "message": "We’re updating MetaMetrics. We'll associate some in-app activity to on-chain data to better understand usage. You can opt out via Settings." + }, "popularNetworkAddToolTip": { "message": "Some of these networks rely on third parties. The connections may be less reliable or enable third-parties to track activity.", "description": "Learn more link" diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 390081fac860..c98f5a270b12 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -93,6 +93,7 @@ export const SENTRY_BACKGROUND_STATE = { shieldEndingToastLastClickedOrClosed: true, shieldPausedToastLastClickedOrClosed: true, isWalletResetInProgress: false, + pna25Acknowledged: false, }, MultichainBalancesController: { balances: false, diff --git a/app/scripts/controllers/app-state-controller.test.ts b/app/scripts/controllers/app-state-controller.test.ts index 3ab39cc8b334..fcade9450da5 100644 --- a/app/scripts/controllers/app-state-controller.test.ts +++ b/app/scripts/controllers/app-state-controller.test.ts @@ -792,6 +792,7 @@ describe('AppStateController', () => { "outdatedBrowserWarningLastShown": null, "pendingShieldCohort": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": false, "popupGasPollTokens": [], "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": false, @@ -883,6 +884,7 @@ describe('AppStateController', () => { "outdatedBrowserWarningLastShown": null, "pendingShieldCohort": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": false, "popupGasPollTokens": [], "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": false, @@ -963,6 +965,7 @@ describe('AppStateController', () => { "onboardingDate": null, "outdatedBrowserWarningLastShown": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": false, "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": false, "recoveryPhraseReminderLastShown": 1000, @@ -1053,6 +1056,7 @@ describe('AppStateController', () => { "outdatedBrowserWarningLastShown": null, "pendingShieldCohort": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": false, "popupGasPollTokens": [], "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": false, diff --git a/app/scripts/controllers/app-state-controller.ts b/app/scripts/controllers/app-state-controller.ts index cb52aca37e08..355bf14e0920 100644 --- a/app/scripts/controllers/app-state-controller.ts +++ b/app/scripts/controllers/app-state-controller.ts @@ -114,6 +114,7 @@ export type AppStateControllerState = { networkConnectionBanner: NetworkConnectionBanner; newPrivacyPolicyToastClickedOrClosed: boolean | null; newPrivacyPolicyToastShownDate: number | null; + pna25Acknowledged: boolean; nftsDetectionNoticeDismissed: boolean; nftsDropdownState: Json; notificationGasPollTokens: string[]; @@ -282,6 +283,7 @@ const getDefaultAppStateControllerState = (): AppStateControllerState => ({ lastViewedUserSurvey: null, newPrivacyPolicyToastClickedOrClosed: null, newPrivacyPolicyToastShownDate: null, + pna25Acknowledged: false, nftsDetectionNoticeDismissed: false, notificationGasPollTokens: [], onboardingDate: null, @@ -479,6 +481,12 @@ const controllerMetadata: StateMetadata = { includeInDebugSnapshot: true, usedInUi: true, }, + pna25Acknowledged: { + includeInStateLogs: true, + persist: true, + includeInDebugSnapshot: true, + usedInUi: true, + }, nftsDetectionNoticeDismissed: { includeInStateLogs: true, persist: true, @@ -887,6 +895,12 @@ export class AppStateController extends BaseController< }); } + setPna25Acknowledged(acknowledged: boolean): void { + this.update((state) => { + state.pna25Acknowledged = acknowledged; + }); + } + setShieldPausedToastLastClickedOrClosed(time: number): void { this.update((state) => { state.shieldPausedToastLastClickedOrClosed = time; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cb8057c7a589..f67fbbdf2f1f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2986,6 +2986,8 @@ export default class MetamaskController extends EventEmitter { appStateController.setShieldEndingToastLastClickedOrClosed.bind( appStateController, ), + setPna25Acknowledged: + appStateController.setPna25Acknowledged.bind(appStateController), setAppActiveTab: appStateController.setAppActiveTab.bind(appStateController), setDefaultSubscriptionPaymentOptions: diff --git a/builds.yml b/builds.yml index 52ff0fbf1853..24437e8e8338 100644 --- a/builds.yml +++ b/builds.yml @@ -35,6 +35,7 @@ buildTypes: - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/10.2.3/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management - IS_SIDEPANEL: true + - EXTENSION_UX_PNA25: true # for seedless onboarding (social login) - GOOGLE_PROD_CLIENT_ID - APPLE_PROD_CLIENT_ID @@ -69,6 +70,7 @@ buildTypes: - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/10.2.3/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management - IS_SIDEPANEL: false + - EXTENSION_UX_PNA25: true # for seedless onboarding (social login) - GOOGLE_BETA_CLIENT_ID - APPLE_BETA_CLIENT_ID @@ -103,6 +105,7 @@ buildTypes: - REJECT_INVALID_SNAPS_PLATFORM_VERSION: true - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/10.2.3/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management + - EXTENSION_UX_PNA25: true # for seedless onboarding (social login) - GOOGLE_EXPERIMENTAL_CLIENT_ID - APPLE_EXPERIMENTAL_CLIENT_ID @@ -138,6 +141,7 @@ buildTypes: - ACCOUNT_SNAPS_DIRECTORY_URL: https://metamask.github.io/snaps-directory-staging/main/account-management - EIP_4337_ENTRYPOINT: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' - IS_SIDEPANEL: false + - EXTENSION_UX_PNA25: true # for seedless onboarding (social login) - GOOGLE_FLASK_CLIENT_ID - APPLE_FLASK_CLIENT_ID @@ -460,3 +464,6 @@ env: # This should only be used for local testing, and should not be enabled in any # production builds (including beta and Flask). - FORCE_PREINSTALLED_SNAPS: 'false' + + # PNA25 (Privacy Notice) + - EXTENSION_UX_PNA25: true diff --git a/shared/lib/ui-utils.js b/shared/lib/ui-utils.js index 0a0dd8761592..666241043b07 100644 --- a/shared/lib/ui-utils.js +++ b/shared/lib/ui-utils.js @@ -5,6 +5,8 @@ export const SUPPORT_LINK = process.env.SUPPORT_LINK; export const COINGECKO_LINK = 'https://www.coingecko.com/'; export const CRYPTOCOMPARE_LINK = 'https://www.cryptocompare.com/'; export const PRIVACY_POLICY_LINK = 'https://consensys.io/privacy-policy/'; +export const METAMETRICS_SETTINGS_LINK = + 'https://support.metamask.io/configure/privacy/how-to-manage-your-metametrics-settings/'; export const SURVEY_LINK = 'https://www.getfeedback.com/r/Oczu1vP0'; // TODO make sure these links are correct diff --git a/shared/types/background.ts b/shared/types/background.ts index 84c313ed471b..49bca449c3d3 100644 --- a/shared/types/background.ts +++ b/shared/types/background.ts @@ -117,6 +117,7 @@ export type ControllerStatePropertiesEnumerated = { isRampCardClosed: AppStateControllerState['isRampCardClosed']; newPrivacyPolicyToastClickedOrClosed: AppStateControllerState['newPrivacyPolicyToastClickedOrClosed']; newPrivacyPolicyToastShownDate: AppStateControllerState['newPrivacyPolicyToastShownDate']; + pna25Acknowledged: AppStateControllerState['pna25Acknowledged']; // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention hadAdvancedGasFeesSetPriorToMigration92_3: AppStateControllerState['hadAdvancedGasFeesSetPriorToMigration92_3']; diff --git a/test/e2e/fixtures/default-fixture.js b/test/e2e/fixtures/default-fixture.js index 0c8bd665f9e7..b78a16430f26 100644 --- a/test/e2e/fixtures/default-fixture.js +++ b/test/e2e/fixtures/default-fixture.js @@ -113,6 +113,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { notificationGasPollTokens: [], popupGasPollTokens: [], recoveryPhraseReminderHasBeenShown: true, + pna25Acknowledged: false, recoveryPhraseReminderLastShown: '__FIXTURE_SUBSTITUTION__currentDateInMilliseconds', showTestnetMessageInDropdown: true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index e06735afb191..985cb15ab7f7 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -52,6 +52,7 @@ "outdatedBrowserWarningLastShown": "object", "pendingShieldCohort": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": "boolean", "popupGasPollTokens": "object", "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index fdbaa1d1c4fe..3eac46e3998c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -114,8 +114,6 @@ "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", - "claims": "object", - "claimsConfigurations": "object", "completedOnboarding": true, "knownMethodData": "object", "use4ByteResolution": true, @@ -134,6 +132,7 @@ } }, "throttledOrigins": "object", + "isSeedlessOnboardingUserAuthenticated": "boolean", "activeQrCodeScanRequest": null, "appActiveTab": "object", "browserEnvironment": { "os": "string", "browser": "string" }, @@ -145,12 +144,14 @@ "enforcedSimulationsSlippageForTransactions": "object", "fullScreenGasPollTokens": "object", "hadAdvancedGasFeesSetPriorToMigration92_3": false, + "canTrackWalletFundsObtained": false, "isRampCardClosed": false, "isUpdateAvailable": false, "lastUpdatedAt": null, "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", + "pna25Acknowledged": "boolean", "nftsDetectionNoticeDismissed": false, "notificationGasPollTokens": "object", "onboardingDate": null, @@ -173,14 +174,17 @@ "updateModalLastDismissedAt": null, "hasShownMultichainAccountsIntroModal": "boolean", "showShieldEntryModalOnce": "boolean", + "pendingShieldCohort": null, + "pendingShieldCohortTxType": null, + "isWalletResetInProgress": "boolean", + "dappSwapComparisonData": "object", "addressSecurityAlertResponses": "object", "currentExtensionPopupId": "number", - "dappSwapComparisonData": "object", "nftsDropdownState": {}, "signatureSecurityAlertResponses": "object", "networkConnectionBanner": "object", - "termsOfUseLastAgreed": "number", "snapsInstallPrivacyWarningShown": true, + "termsOfUseLastAgreed": "number", "currentAppVersion": "string", "previousAppVersion": "", "previousMigrationVersion": 0, @@ -214,7 +218,6 @@ "identities": "object", "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", - "isMultiAccountBalancesEnabled": "boolean", "ledgerTransportType": "webhid", "lostIdentities": "object", "manageInstitutionalWallets": "boolean", @@ -229,6 +232,7 @@ "useCurrencyRateCheck": true, "useExternalNameSources": "boolean", "useExternalServices": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "useMultiAccountBalanceChecker": true, "useNftDetection": false, "usePhishDetect": true, @@ -253,7 +257,6 @@ "seedPhraseBackedUp": true, "onboardingTabs": "object", "socialBackupsMetadata": "object", - "isSeedlessOnboardingUserAuthenticated": "boolean", "subscriptions": "object", "trialedProducts": "object", "subjects": "object", @@ -328,7 +331,6 @@ "isUpdatingMetamaskNotifications": "boolean", "isFetchingMetamaskNotifications": "boolean", "isUpdatingMetamaskNotificationsAccount": "object", - "isWalletResetInProgress": "boolean", "isCheckingAccountsPresence": "boolean", "isPushEnabled": "boolean", "fcmToken": "string", @@ -339,12 +341,6 @@ "feature3": { "name": "groupC", "value": "valueC" }, "sendRedesign": { "enabled": false } }, - "rewardsAccounts": "object", - "rewardsActiveAccount": null, - "rewardsSeasonStatuses": "object", - "rewardsSeasons": "object", - "rewardsSubscriptionTokens": "object", - "rewardsSubscriptions": "object", "cacheTimestamp": "number", "allDeFiPositions": "object", "allDeFiPositionsCount": "object", @@ -352,6 +348,8 @@ "tokenScanCache": "object", "coverageResults": "object", "orderedTransactionHistory": "object", + "claimsConfigurations": "object", + "claims": "object", "accountsByChainId": "object", "unapprovedDecryptMsgs": "object", "unapprovedDecryptMsgCount": 0, @@ -405,9 +403,6 @@ "ensEntries": "object", "ensResolutionsByAddress": "object", "pendingApprovals": "object", - "pendingRevocations": "object", - "pendingShieldCohort": null, - "pendingShieldCohortTxType": null, "pendingApprovalCount": "number", "approvalFlows": "object", "storageMetadata": {}, @@ -420,12 +415,18 @@ "hasAccountTreeSyncingSyncedAtLeastOnce": "boolean", "accountGroupsMetadata": "object", "accountWalletsMetadata": "object", - "canTrackWalletFundsObtained": false, "delegations": "object", "isGatorPermissionsEnabled": "boolean", "gatorPermissionsMapSerialized": "string", "isFetchingGatorPermissions": "boolean", "gatorPermissionsProviderSnapId": "string", + "pendingRevocations": "object", + "rewardsActiveAccount": null, + "rewardsAccounts": "object", + "rewardsSubscriptions": "object", + "rewardsSeasons": "object", + "rewardsSeasonStatuses": "object", + "rewardsSubscriptionTokens": "object", "srpSessionData": "object" }, "ramps": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index b41f2ad3e1fb..63e21fc22039 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -53,6 +53,7 @@ "showShieldEntryModalOnce": "boolean", "pendingShieldCohort": null, "pendingShieldCohortTxType": null, + "pna25Acknowledged": "boolean", "appActiveTab": "object" }, "BridgeController": {}, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index abe11e2d1490..d3ffc35e6fcc 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -1,42 +1,42 @@ { "data": { - "AuthenticationController": { "isSignedIn": "boolean" }, - "NotificationServicesController": { - "subscriptionAccountsSeen": "object", - "isMetamaskNotificationsFeatureSeen": "boolean", - "isNotificationServicesEnabled": "boolean", - "isFeatureAnnouncementsEnabled": "boolean", - "metamaskNotificationsList": "object", - "metamaskNotificationsReadList": "object" + "AccountOrderController": { + "hiddenAccountList": {}, + "pinnedAccountList": {} + }, + "AccountTracker": { "accountsByChainId": "object" }, + "AccountTreeController": { + "accountGroupsMetadata": "object", + "accountWalletsMetadata": "object", + "hasAccountTreeSyncingSyncedAtLeastOnce": "boolean" }, "AccountsController": { "internalAccounts": { "accounts": "object", "selectedAccount": "string" } }, + "AddressBookController": { "addressBook": "object" }, "AlertController": { "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", "web3ShimUsageOrigins": "object" }, "AnnouncementController": { "announcements": "object" }, - "NetworkOrderController": { - "orderedNetworkList": { "0": "object", "1": "object", "2": "object" } - }, - "NetworkEnablementController": { - "enabledNetworkMap": { "eip155": "object", "solana": "object" } - }, - "AccountOrderController": { - "pinnedAccountList": {}, - "hiddenAccountList": {} + "AppMetadataController": { + "currentAppVersion": "string", + "currentMigrationVersion": "number", + "previousAppVersion": "", + "previousMigrationVersion": 0 }, "AppStateController": { - "browserEnvironment": { "os": "string", "browser": "string" }, + "browserEnvironment": { "browser": "string", "os": "string" }, + "canTrackWalletFundsObtained": false, "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, "enableEnforcedSimulations": true, "enforcedSimulationsSlippage": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, - "canTrackWalletFundsObtained": false, + "hasShownMultichainAccountsIntroModal": "boolean", "isRampCardClosed": false, + "isWalletResetInProgress": "boolean", "lastUpdatedAt": null, "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", @@ -44,31 +44,33 @@ "nftsDetectionNoticeDismissed": false, "onboardingDate": null, "outdatedBrowserWarningLastShown": "object", + "pendingShieldCohortTxType": null, + "pna25Acknowledged": "boolean", "productTour": "accountIcon", "recoveryPhraseReminderHasBeenShown": true, "recoveryPhraseReminderLastShown": "number", + "shieldEndingToastLastClickedOrClosed": "object", + "shieldPausedToastLastClickedOrClosed": "object", "showAccountBanner": true, "showBetaHeader": false, "showDownloadMobileAppSlide": "boolean", "showNetworkBanner": true, "showPermissionsTour": true, + "showShieldEntryModalOnce": "boolean", "showTestnetMessageInDropdown": true, "slides": "object", + "snapsInstallPrivacyWarningShown": true, "surveyLinkLastClickedOrClosed": "object", - "shieldEndingToastLastClickedOrClosed": "object", - "shieldPausedToastLastClickedOrClosed": "object", - "trezorModel": null, - "updateModalLastDismissedAt": null, - "hasShownMultichainAccountsIntroModal": "boolean", - "showShieldEntryModalOnce": "boolean", - "pendingShieldCohortTxType": null, - "isWalletResetInProgress": "boolean", "termsOfUseLastAgreed": "number", - "snapsInstallPrivacyWarningShown": true + "trezorModel": null, + "updateModalLastDismissedAt": null }, + "ApprovalController": {}, + "AuthenticationController": { "isSignedIn": "boolean" }, "BridgeController": {}, + "BridgeStatusController": { "txHistory": "object" }, + "ClaimsController": "object", "CurrencyController": { - "currentCurrency": "usd", "currencyRates": { "ETH": { "conversionDate": "number", @@ -80,44 +82,103 @@ "conversionRate": 0.2, "usdConversionRate": 0.2 } - } + }, + "currentCurrency": "usd" + }, + "DeFiPositionsController": "object", + "DecryptMessageController": {}, + "DelegationController": "object", + "EncryptionPublicKeyController": {}, + "EnsController": { + "ensEntries": "object", + "ensResolutionsByAddress": "object" }, "GasFeeController": { - "gasFeeEstimatesByChainId": {}, - "gasFeeEstimates": {}, "estimatedGasFeeTimeBounds": {}, "gasEstimateType": "none", + "gasFeeEstimates": {}, + "gasFeeEstimatesByChainId": {}, "nonRPCGasFeeApisDisabled": "boolean" }, + "GatorPermissionsController": "object", "KeyringController": { "vault": "string" }, + "LoggingController": { "logs": "object" }, "MetaMetricsController": { - "participateInMetaMetrics": true, - "metaMetricsId": "0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420", "dataCollectionForMarketing": "boolean", - "marketingCampaignCookieId": null, "eventsBeforeMetricsOptIn": "object", - "tracesBeforeMetricsOptIn": "object", - "traits": "object", "fragments": "object", - "segmentApiCalls": "object" + "marketingCampaignCookieId": null, + "metaMetricsId": "0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420", + "participateInMetaMetrics": true, + "segmentApiCalls": "object", + "tracesBeforeMetricsOptIn": "object", + "traits": "object" }, "MetaMetricsDataDeletionController": { "metaMetricsDataDeletionId": null, "metaMetricsDataDeletionTimestamp": 0 }, + "MultichainAccountService": "object", + "MultichainAssetsController": { + "accountsAssets": "object", + "allIgnoredAssets": "object", + "assetsMetadata": "object" + }, + "MultichainAssetsRatesController": { "conversionRates": "object" }, + "MultichainBalancesController": { "balances": "object" }, + "MultichainNetworkController": "object", + "MultichainRatesController": { + "cryptocurrencies": ["btc", "sol"], + "fiatCurrency": "usd", + "rates": { + "btc": { "conversionDate": 0, "conversionRate": 0 }, + "sol": { "conversionDate": 0, "conversionRate": 0 } + } + }, + "MultichainTransactionsController": "object", + "NameController": { "nameSources": "object", "names": "object" }, "NetworkController": { - "selectedNetworkClientId": "string", + "networkConfigurationsByChainId": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "unknown" } }, - "networkConfigurationsByChainId": "object" + "selectedNetworkClientId": "string" + }, + "NetworkEnablementController": { + "enabledNetworkMap": { "eip155": "object", "solana": "object" } + }, + "NetworkOrderController": { + "orderedNetworkList": { "0": "object", "1": "object", "2": "object" } + }, + "NftController": { + "allNftContracts": "object", + "allNfts": "object", + "ignoredNfts": "object" + }, + "NotificationServicesController": { + "isFeatureAnnouncementsEnabled": "boolean", + "isMetamaskNotificationsFeatureSeen": "boolean", + "isNotificationServicesEnabled": "boolean", + "metamaskNotificationsList": "object", + "metamaskNotificationsReadList": "object", + "subscriptionAccountsSeen": "object" + }, + "NotificationServicesPushController": { + "fcmToken": "string", + "isPushEnabled": "boolean" }, "OnboardingController": { - "seedPhraseBackedUp": true, + "completedOnboarding": true, "firstTimeFlowType": "import", - "completedOnboarding": true + "seedPhraseBackedUp": true }, + "PPOMController": { "storageMetadata": {} }, "PermissionController": { "subjects": "object" }, + "PermissionLogController": { "permissionHistory": "object" }, + "PhishingController": { + "tokenScanCache": "object", + "urlScanCache": "object" + }, "PreferencesController": { "addSnapAccountEnabled": "boolean", "advancedGasFee": {}, @@ -129,6 +190,7 @@ "identities": "object", "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "knownMethodData": "object", "ledgerTransportType": "webhid", "lostIdentities": "object", @@ -141,6 +203,7 @@ "hideZeroBalanceTokens": false, "petnamesEnabled": "boolean", "privacyMode": "boolean", + "shouldShowAggregatedBalancePopover": "boolean", "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showMultiRpcModal": "boolean", @@ -148,14 +211,14 @@ "showTestNetworks": false, "skipDeepLinkInterstitial": "boolean", "smartAccountOptIn": "boolean", - "smartTransactionsOptInStatus": true, "smartTransactionsMigrationApplied": "boolean", + "smartTransactionsOptInStatus": true, "tokenNetworkFilter": {}, "tokenSortConfig": "object", "useNativeCurrencyAsPrimaryCurrency": "boolean", - "useSidePanelAsDefault": "boolean", - "shouldShowAggregatedBalancePopover": "boolean" + "useSidePanelAsDefault": "boolean" }, + "referrals": "object", "securityAlertsEnabled": "boolean", "selectedAddress": "string", "snapRegistryList": "object", @@ -167,139 +230,77 @@ "useCurrencyRateCheck": true, "useExternalNameSources": "boolean", "useExternalServices": "boolean", - "isMultiAccountBalancesEnabled": "boolean", "useMultiAccountBalanceChecker": true, "useNftDetection": false, "usePhishDetect": true, "useSafeChainsListValidation": "boolean", "useTokenDetection": true, "useTransactionSimulations": true, - "watchEthereumAccountEnabled": "boolean", - "referrals": "object" - }, - "SelectedNetworkController": { "domains": "object" }, - "SmartTransactionsController": {}, - "SubjectMetadataController": { "subjectMetadata": "object" }, - "TokensController": { - "allTokens": {}, - "allIgnoredTokens": {}, - "allDetectedTokens": {} - }, - "MultichainAccountService": "object", - "TransactionController": { - "methodData": "object", - "transactions": "object", - "transactionBatches": "object", - "lastFetchedBlockNumbers": "object", - "submitHistory": "object" - }, - "config": "object", - "firstTimeInfo": "object", - "NameController": { "names": "object", "nameSources": "object" }, - "UserStorageController": { - "isBackupAndSyncEnabled": true, - "isAccountSyncingEnabled": true, - "isContactSyncingEnabled": true - }, - "AppMetadataController": { - "currentAppVersion": "string", - "previousAppVersion": "", - "previousMigrationVersion": 0, - "currentMigrationVersion": "number" - }, - "AddressBookController": { "addressBook": "object" }, - "MultichainNetworkController": "object", - "SeedlessOnboardingController": "object", - "PermissionLogController": { "permissionHistory": "object" }, - "GatorPermissionsController": "object", - "TokenListController": { - "tokensChainsCache": {}, - "preventPollingOnNetworkRestart": false - }, - "TokenBalancesController": { "tokenBalances": "object" }, - "NftController": { - "allNftContracts": "object", - "allNfts": "object", - "ignoredNfts": "object" - }, - "PhishingController": { - "urlScanCache": "object", - "tokenScanCache": "object" - }, - "LoggingController": { "logs": "object" }, - "MultichainRatesController": { - "fiatCurrency": "usd", - "rates": { - "btc": { "conversionDate": 0, "conversionRate": 0 }, - "sol": { "conversionDate": 0, "conversionRate": 0 } - }, - "cryptocurrencies": ["btc", "sol"] - }, - "UserOperationController": { "userOperations": "object" }, - "NotificationServicesPushController": { - "isPushEnabled": "boolean", - "fcmToken": "string" + "watchEthereumAccountEnabled": "boolean" }, "RemoteFeatureFlagController": { + "cacheTimestamp": "number", "remoteFeatureFlags": { "feature1": true, "feature2": false, "feature3": { "name": "groupC", "value": "valueC" }, "sendRedesign": { "enabled": false } - }, - "cacheTimestamp": "number" - }, - "DeFiPositionsController": "object", - "AccountTracker": { "accountsByChainId": "object" }, - "TokenRatesController": { "marketData": "object" }, - "DecryptMessageController": {}, - "EncryptionPublicKeyController": {}, - "SignatureController": {}, - "SwapsController": {}, - "BridgeStatusController": { "txHistory": "object" }, - "EnsController": { - "ensEntries": "object", - "ensResolutionsByAddress": "object" + } }, - "ApprovalController": {}, - "SnapsRegistry": { - "database": "object", - "lastUpdated": "number", - "databaseUnavailable": "boolean" + "RewardsController": { + "rewardsAccounts": "object", + "rewardsActiveAccount": null, + "rewardsSeasonStatuses": "object", + "rewardsSeasons": "object", + "rewardsSubscriptionTokens": "object", + "rewardsSubscriptions": "object" }, + "SeedlessOnboardingController": "object", + "SelectedNetworkController": { "domains": "object" }, + "ShieldController": "object", + "SignatureController": {}, + "SmartTransactionsController": {}, "SnapController": { - "snaps": "object", "snapStates": "object", + "snaps": "object", "unencryptedSnapStates": "object" }, "SnapInsightsController": {}, "SnapInterfaceController": { "interfaces": "object" }, - "PPOMController": { "storageMetadata": {} }, - "AccountTreeController": { - "hasAccountTreeSyncingSyncedAtLeastOnce": "boolean", - "accountGroupsMetadata": "object", - "accountWalletsMetadata": "object" - }, - "MultichainAssetsController": { - "accountsAssets": "object", - "assetsMetadata": "object", - "allIgnoredAssets": "object" + "SnapsRegistry": { + "database": "object", + "databaseUnavailable": "boolean", + "lastUpdated": "number" }, - "MultichainAssetsRatesController": { "conversionRates": "object" }, - "MultichainBalancesController": { "balances": "object" }, - "MultichainTransactionsController": "object", - "DelegationController": "object", + "SubjectMetadataController": { "subjectMetadata": "object" }, "SubscriptionController": "object", - "ShieldController": "object", - "ClaimsController": "object", - "RewardsController": { - "rewardsActiveAccount": null, - "rewardsAccounts": "object", - "rewardsSubscriptions": "object", - "rewardsSeasons": "object", - "rewardsSeasonStatuses": "object", - "rewardsSubscriptionTokens": "object" - } + "SwapsController": {}, + "TokenBalancesController": { "tokenBalances": "object" }, + "TokenListController": { + "preventPollingOnNetworkRestart": false, + "tokensChainsCache": {} + }, + "TokenRatesController": { "marketData": "object" }, + "TokensController": { + "allDetectedTokens": {}, + "allIgnoredTokens": {}, + "allTokens": {} + }, + "TransactionController": { + "lastFetchedBlockNumbers": "object", + "methodData": "object", + "submitHistory": "object", + "transactionBatches": "object", + "transactions": "object" + }, + "UserOperationController": { "userOperations": "object" }, + "UserStorageController": { + "isAccountSyncingEnabled": true, + "isBackupAndSyncEnabled": true, + "isContactSyncingEnabled": true + }, + "config": "object", + "firstTimeInfo": "object" }, "meta": { "version": 183 } } diff --git a/test/e2e/tests/settings/state-logs.json b/test/e2e/tests/settings/state-logs.json index b53f584006aa..d4acae38c362 100644 --- a/test/e2e/tests/settings/state-logs.json +++ b/test/e2e/tests/settings/state-logs.json @@ -709,6 +709,7 @@ "pendingRevocations": [], "pendingShieldCohort": "null", "pendingShieldCohortTxType": "null", + "pna25Acknowledged": "boolean", "permissionActivityLog": [ { "id": "number", diff --git a/ui/components/app/toast-master/selectors.ts b/ui/components/app/toast-master/selectors.ts index 671e4e61ad88..fc9ee581fd72 100644 --- a/ui/components/app/toast-master/selectors.ts +++ b/ui/components/app/toast-master/selectors.ts @@ -47,6 +47,10 @@ type State = { | 'surveyLinkLastClickedOrClosed' | 'shieldEndingToastLastClickedOrClosed' | 'shieldPausedToastLastClickedOrClosed' + | 'participateInMetaMetrics' + | 'remoteFeatureFlags' + | 'pna25Acknowledged' + | 'completedOnboarding' > >; }; @@ -234,3 +238,50 @@ export function selectShowShieldEndingToast( ): boolean { return !state.metamask.shieldEndingToastLastClickedOrClosed; } + +/** + * Determines if the PNA25 banner should be shown based on: + * - User has completed onboarding (completedOnboarding === true) + * - LaunchDarkly feature flag (extensionUxPna25) is enabled + * - User has opted into metrics (participateInMetaMetrics === true) + * - User hasn't acknowledged the banner yet (pna25Acknowledged === false) + * + * Regular new users: Go through metametrics page → pna25Acknowledged = true → don't see banner + * Social login users: Skip metametrics page → pna25Acknowledged = false → see banner + * Existing users: pna25Acknowledged = false (default) → see banner + * + * @param state - The application state containing the banner data. + * @returns Boolean indicating whether to show the banner + */ +export function selectShowPna25Banner(state: Pick): boolean { + const { + completedOnboarding, + participateInMetaMetrics, + pna25Acknowledged, + remoteFeatureFlags, + } = state.metamask || {}; + + // Only show to users who have completed onboarding + if (!completedOnboarding) { + return false; // User hasn't completed onboarding yet + } + + // For onboarding screen, we use local flag and for existing users, we use LaunchDarkly flag + const isPna25Enabled = remoteFeatureFlags?.extensionUxPna25; + + // Check all conditions + if (!isPna25Enabled) { + return false; // LD flag not enabled + } + + if (participateInMetaMetrics !== true) { + return false; // User hasn't opted into metrics + } + + if (pna25Acknowledged === true) { + return false; // User already acknowledged + } + + // Only show banner if explicitly false (existing users who haven't acknowledged) + return pna25Acknowledged === false; +} diff --git a/ui/components/app/toast-master/toast-master.js b/ui/components/app/toast-master/toast-master.js index 268b90621069..853e778c8c3f 100644 --- a/ui/components/app/toast-master/toast-master.js +++ b/ui/components/app/toast-master/toast-master.js @@ -11,6 +11,7 @@ import { MILLISECOND, SECOND } from '../../../../shared/constants/time'; import { PRIVACY_POLICY_LINK, SURVEY_LINK, + METAMETRICS_SETTINGS_LINK, } from '../../../../shared/lib/ui-utils'; import { BorderColor, @@ -40,6 +41,7 @@ import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/ import { addPermittedAccount, hidePermittedNetworkToast, + setPna25Acknowledged, } from '../../../store/actions'; import { AvatarNetwork, @@ -94,6 +96,7 @@ import { selectClaimSubmitToast, selectShowShieldPausedToast, selectShowShieldEndingToast, + selectShowPna25Banner, } from './selectors'; import { setNewPrivacyPolicyToastClickedOrClosed, @@ -129,6 +132,7 @@ export function ToastMaster({ location } = {}) { )} + @@ -749,3 +753,39 @@ function ShieldEndingToast() { ) ); } + +function Pna25Banner() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const showPna25Banner = useSelector(selectShowPna25Banner); + + const handleLearnMore = () => { + // Open MetaMetrics settings help page and acknowledge + global.platform.openTab({ + url: METAMETRICS_SETTINGS_LINK, + }); + }; + + const handleClose = () => { + // Just acknowledge without opening link + dispatch(setPna25Acknowledged(true)); + }; + + return ( + showPna25Banner && ( + + } + text={t('pna25BannerTitle')} + textVariant={TextVariant.bodySm} + actionText={t('learnMoreUpperCase')} + onActionClick={handleLearnMore} + onClose={handleClose} + /> + ) + ); +} diff --git a/ui/components/app/toast-master/utils.ts b/ui/components/app/toast-master/utils.ts index e5a905f6bdac..a8ad631849dc 100644 --- a/ui/components/app/toast-master/utils.ts +++ b/ui/components/app/toast-master/utils.ts @@ -117,3 +117,7 @@ export function setShieldEndingToastLastClickedOrClosed(time: number) { time, ]); } + +export function setPna25Acknowledged(acknowledged: boolean) { + submitRequestToBackgroundAndCatch('setPna25Acknowledged', [acknowledged]); +} diff --git a/ui/hooks/useMetametrics.ts b/ui/hooks/useMetametrics.ts index ca3126f8483c..69470f418943 100644 --- a/ui/hooks/useMetametrics.ts +++ b/ui/hooks/useMetametrics.ts @@ -1,11 +1,14 @@ import { useState, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import log from 'loglevel'; import { setParticipateInMetaMetrics, + setPna25Acknowledged, showLoadingIndication, hideLoadingIndication, } from '../store/actions'; +import { getPna25Acknowledged } from '../selectors/metametrics'; +import { getRemoteFeatureFlags } from '../selectors/remote-feature-flags'; /** * Provides a hook to enable MetaMetrics tracking. @@ -22,6 +25,8 @@ export function useEnableMetametrics(): { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const pna25Acknowledged = useSelector(getPna25Acknowledged); + const remoteFeatureFlags = useSelector(getRemoteFeatureFlags); const enableMetametrics = useCallback(async () => { setLoading(true); @@ -30,6 +35,10 @@ export function useEnableMetametrics(): { try { await dispatch(setParticipateInMetaMetrics(true)); + const isPna25Enabled = remoteFeatureFlags?.extensionUxPna25; + if (isPna25Enabled && pna25Acknowledged === false) { + await dispatch(setPna25Acknowledged(true)); + } } catch (e) { setError(e instanceof Error ? e.message : 'An unexpected error occurred'); log.error(e); @@ -40,7 +49,7 @@ export function useEnableMetametrics(): { } dispatch(hideLoadingIndication()); - }, [dispatch]); + }, [dispatch, pna25Acknowledged, remoteFeatureFlags]); return { enableMetametrics, diff --git a/ui/pages/onboarding-flow/metametrics/metametrics.js b/ui/pages/onboarding-flow/metametrics/metametrics.js index 915e2fb04bae..a5acda48061a 100644 --- a/ui/pages/onboarding-flow/metametrics/metametrics.js +++ b/ui/pages/onboarding-flow/metametrics/metametrics.js @@ -20,6 +20,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { setParticipateInMetaMetrics, setDataCollectionForMarketing, + setPna25Acknowledged, } from '../../../store/actions'; import { getCurrentKeyring, @@ -65,7 +66,8 @@ export default function OnboardingMetametrics() { ); const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics); const dataCollectionForMarketing = useSelector(getDataCollectionForMarketing); - + // Check if the PNA25 feature is enabled + const isPna25Enabled = process.env.EXTENSION_UX_PNA25; const [ isParticipateInMetaMetricsChecked, setIsParticipateInMetaMetricsChecked, @@ -112,6 +114,18 @@ export default function OnboardingMetametrics() { const handleContinue = async (e) => { e.preventDefault(); try { + // Set pna25Acknowledged to true for all new users who complete onboarding + // This indicates they saw the updated policy during onboarding + // Only set if feature flag is enabled, as the banner only shows when flag is enabled + if (isPna25Enabled) { + try { + await dispatch(setPna25Acknowledged(true)); + } catch (error) { + // Log error but don't block onboarding if state update fails + log.error('Error setting pna25Acknowledged:', error); + } + } + if (isParticipateInMetaMetricsChecked) { dispatch( setDataCollectionForMarketing(isDataCollectionForMarketingChecked), @@ -229,7 +243,9 @@ export default function OnboardingMetametrics() { color={TextColor.textAlternative} textAlign={TextAlign.Left} > - {t('onboardingMetametricCheckboxDescriptionOne')} + {isPna25Enabled + ? t('onboardingMetametricCheckboxDescriptionOneUpdated') + : t('onboardingMetametricCheckboxDescriptionOne')} diff --git a/ui/pages/onboarding-flow/welcome/welcome.js b/ui/pages/onboarding-flow/welcome/welcome.js index 5eaf0578b757..3d99991580fd 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.js +++ b/ui/pages/onboarding-flow/welcome/welcome.js @@ -31,6 +31,7 @@ import { setFirstTimeFlowType, startOAuthLogin, setParticipateInMetaMetrics, + setPna25Acknowledged, getIsSeedlessOnboardingUserAuthenticated, } from '../../../store/actions'; import { @@ -420,6 +421,10 @@ export default function OnboardingWelcome() { if (!isFireFox) { // automatically set participate in meta metrics to true for social login users in chrome dispatch(setParticipateInMetaMetrics(true)); + // Set pna25Acknowledged to true for social login users if feature flag is enabled + if (process.env.EXTENSION_UX_PNA25) { + dispatch(setPna25Acknowledged(true)); + } } } catch (error) { handleLoginError(error); diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index cdf1b1dba7c0..f036091251e2 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -14,6 +14,8 @@ export const getParticipateInMetaMetrics = (state) => export const getIsParticipateInMetaMetricsSet = (state) => state.metamask.participateInMetaMetrics !== null; +export const getPna25Acknowledged = (state) => state.metamask.pna25Acknowledged; + export const getLatestMetricsEventTimestamp = (state) => state.metamask.latestNonAnonymousEventTimestamp; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index b3d0d5dcef01..16d606182755 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4824,6 +4824,15 @@ export function setDataCollectionForMarketing( }; } +export function setPna25Acknowledged( + acknowledged: boolean, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { + return async () => { + log.debug(`background.setPna25Acknowledged`); + await submitRequestToBackground('setPna25Acknowledged', [acknowledged]); + }; +} + /** * Sets marketing consent with OAuth service for social login users. *