diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2499ca4a199e..192fe5d9facc 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -11833,6 +11833,10 @@ "value": { "message": "Value" }, + "variationalReferralSubtitle": { + "message": "Get +15% Omni points and Silver tier for 90 days with a MetaMask referral code.", + "description": "The subtitle of the Variational referral confirmation screen" + }, "version": { "message": "Version" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 2499ca4a199e..192fe5d9facc 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -11833,6 +11833,10 @@ "value": { "message": "Value" }, + "variationalReferralSubtitle": { + "message": "Get +15% Omni points and Silver tier for 90 days with a MetaMask referral code.", + "description": "The subtitle of the Variational referral confirmation screen" + }, "version": { "message": "Version" }, diff --git a/app/images/variational-referral.png b/app/images/variational-referral.png new file mode 100644 index 000000000000..614550d43bfd Binary files /dev/null and b/app/images/variational-referral.png differ diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 5eaeae8af218..f5bcc31c1572 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -815,6 +815,7 @@ describe('preferences controller', () => { "asterdex": {}, "gmx": {}, "hyperliquid": {}, + "variational": {}, }, "securityAlertsEnabled": true, "snapRegistryList": {}, @@ -899,6 +900,7 @@ describe('preferences controller', () => { "asterdex": {}, "gmx": {}, "hyperliquid": {}, + "variational": {}, }, "securityAlertsEnabled": true, "showSidePanelMigrationToast": false, @@ -984,6 +986,7 @@ describe('preferences controller', () => { "asterdex": {}, "gmx": {}, "hyperliquid": {}, + "variational": {}, }, "securityAlertsEnabled": true, "showSidePanelMigrationToast": false, @@ -1120,6 +1123,7 @@ describe('preferences controller', () => { }, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.AsterDEX]: {}, + [DefiReferralPartner.Variational]: {}, }, }, }); @@ -1144,6 +1148,7 @@ describe('preferences controller', () => { }, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.AsterDEX]: {}, + [DefiReferralPartner.Variational]: {}, }, }, }); @@ -1185,6 +1190,7 @@ describe('preferences controller', () => { }, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.AsterDEX]: {}, + [DefiReferralPartner.Variational]: {}, }, }, }); @@ -1208,6 +1214,7 @@ describe('preferences controller', () => { }, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.AsterDEX]: {}, + [DefiReferralPartner.Variational]: {}, }, }, }); @@ -1232,6 +1239,9 @@ describe('preferences controller', () => { expect( controller.state.referrals[DefiReferralPartner.AsterDEX], ).toStrictEqual({}); + expect( + controller.state.referrals[DefiReferralPartner.Variational], + ).toStrictEqual({}); }); it('deep merges referrals state to add new partners while preserving existing data', () => { diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index ace8fd110403..88bb419dac26 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -209,6 +209,7 @@ export const getDefaultPreferencesControllerState = [DefiReferralPartner.AsterDEX]: {}, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.Hyperliquid]: {}, + [DefiReferralPartner.Variational]: {}, }, }); diff --git a/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts b/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts index e29b704e1ab3..024e882e4f38 100644 --- a/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts +++ b/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts @@ -30,6 +30,7 @@ export function getLegacyBackgroundApiServiceMessenger( 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getNetworkClientById', 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'SelectedNetworkController:getNetworkClientIdForDomain', 'NetworkController:getSelectedNetworkClient', 'NetworkController:addNetwork', 'NetworkController:setActiveNetwork', diff --git a/app/scripts/services/legacy-background-api-service.test.ts b/app/scripts/services/legacy-background-api-service.test.ts index 3d692d79e0c0..ef426a04c7f1 100644 --- a/app/scripts/services/legacy-background-api-service.test.ts +++ b/app/scripts/services/legacy-background-api-service.test.ts @@ -6259,6 +6259,7 @@ describe('LegacyBackgroundApiService', () => { describe('DeFi referral', () => { const HYPERLIQUID = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Hyperliquid]; const GMX = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.GMX]; + const VARIATIONAL = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Variational]; const mockTabId = 140; const mockPermittedAccount = '0x123'; const mockPermittedAccounts = [mockPermittedAccount, '0x456']; @@ -6366,6 +6367,26 @@ describe('LegacyBackgroundApiService', () => { ); } + /** + * Registers the network handlers used by the requiredChainId gate. + * + * @param rootMessenger - The root messenger to register handlers on. + * @param chainId - The active chain ID for the partner's domain. + */ + function registerPartnerNetwork( + rootMessenger: RootMessenger, + chainId: string, + ) { + rootMessenger.registerActionHandler( + 'SelectedNetworkController:getNetworkClientIdForDomain', + jest.fn().mockReturnValue('network-client-id'), + ); + rootMessenger.registerActionHandler( + 'NetworkController:getNetworkConfigurationByNetworkClientId', + jest.fn().mockReturnValue({ chainId }), + ); + } + beforeEach(() => { jest.mocked(trackEvent).mockClear(); jest.mocked(checkGmxHasReferralCode).mockReset().mockResolvedValue(false); @@ -7057,6 +7078,69 @@ describe('LegacyBackgroundApiService', () => { ); }); }); + + describe('requiredChainId gate', () => { + it('does not proceed when the active chain does not match requiredChainId', async () => { + const getPermittedAccounts = jest + .fn() + .mockResolvedValue(mockPermittedAccounts); + await withService( + { options: { getPermittedAccounts } }, + async ({ rootMessenger }) => { + const handlers = registerReferralHandlers(rootMessenger, { + featureFlags: { [DefiReferralPartner.Variational]: true }, + referrals: { [DefiReferralPartner.Variational]: {} }, + }); + registerPartnerNetwork(rootMessenger, '0x1'); + + await rootMessenger.call( + 'LegacyBackgroundApiService:handleDefiReferral', + VARIATIONAL, + mockTabId, + ReferralTriggerType.NewConnection, + ); + + expect(handlers.add).not.toHaveBeenCalled(); + expect(handlers.addReferralPassedAccount).not.toHaveBeenCalled(); + }, + ); + }); + + it('proceeds when the active chain matches requiredChainId', async () => { + const getPermittedAccounts = jest + .fn() + .mockResolvedValue(mockPermittedAccounts); + await withService( + { options: { getPermittedAccounts } }, + async ({ rootMessenger }) => { + const handlers = registerReferralHandlers(rootMessenger, { + featureFlags: { [DefiReferralPartner.Variational]: true }, + referrals: { [DefiReferralPartner.Variational]: {} }, + }); + registerPartnerNetwork(rootMessenger, '0xa4b1'); + + await rootMessenger.call( + 'LegacyBackgroundApiService:handleDefiReferral', + VARIATIONAL, + mockTabId, + ReferralTriggerType.NewConnection, + ); + + expect(handlers.add).toHaveBeenCalledWith({ + origin: VARIATIONAL.origin, + type: VARIATIONAL.approvalType, + requestData: { + learnMoreUrl: VARIATIONAL.learnMoreUrl, + partnerId: DefiReferralPartner.Variational, + partnerName: VARIATIONAL.name, + selectedAddress: mockPermittedAccount, + }, + shouldShowRequest: true, + }); + }, + ); + }); + }); }); describe('handleDefiReferralOnPermittedAccountsAdded', () => { @@ -7329,6 +7413,7 @@ function getMessenger( 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getNetworkClientById', 'NetworkController:getNetworkConfigurationByNetworkClientId', + 'SelectedNetworkController:getNetworkClientIdForDomain', 'NetworkController:getSelectedNetworkClient', 'NetworkController:addNetwork', 'NetworkController:setActiveNetwork', diff --git a/app/scripts/services/legacy-background-api-service.ts b/app/scripts/services/legacy-background-api-service.ts index 02cae1021605..f3f6443cdb1c 100644 --- a/app/scripts/services/legacy-background-api-service.ts +++ b/app/scripts/services/legacy-background-api-service.ts @@ -23,6 +23,7 @@ import { NetworkEnablementControllerState, NetworkEnablementControllerStateChangeEvent, } from '@metamask/network-enablement-controller'; +import { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller'; import { add0x, bytesToHex, @@ -678,6 +679,7 @@ type AllowedActions = | SeedlessOnboardingControllerSubmitPasswordAction | SeedlessOnboardingControllerSyncLatestGlobalPasswordAction | SeedlessOnboardingControllerUpdateBackupMetadataStateAction + | SelectedNetworkControllerGetNetworkClientIdForDomainAction | GetSignatureState | ShieldControllerClearStateAction | ShieldControllerStartAction @@ -4598,6 +4600,25 @@ export class LegacyBackgroundApiService { return; } + // If the partner requires a specific chain and user's chain doesn't match, + // return early to avoid the referral code potentially not being applied. + // Don't write any account status so that the prompt can show on the next + // trigger (NewConnection or OnNavigateConnectedTab) once the user has switched chain + if (partner.requiredChainId) { + const networkClientId = this.#messenger.call( + 'SelectedNetworkController:getNetworkClientIdForDomain', + partner.origin, + ); + const networkConfig = this.#messenger.call( + 'NetworkController:getNetworkConfigurationByNetworkClientId', + networkClientId, + ); + const currentChainId = networkConfig?.chainId; + if (currentChainId !== partner.requiredChainId) { + return; + } + } + const { activePermittedAddressOverride } = options; const activePermittedAccount = (activePermittedAddressOverride && diff --git a/shared/constants/app.ts b/shared/constants/app.ts index 2c2d3898405e..973b0c0bb364 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -167,6 +167,7 @@ export const MESSAGE_TYPE = { HYPERLIQUID_REFERRAL_CONSENT: 'hyperliquid_referral_consent', ASTERDEX_REFERRAL_CONSENT: 'asterdex_referral_consent', GMX_REFERRAL_CONSENT: 'gmx_referral_consent', + VARIATIONAL_REFERRAL_CONSENT: 'variational_referral_consent', } as const; export type MessageType = (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE]; @@ -176,6 +177,8 @@ export const HYPERLIQUID_APPROVAL_TYPE = MESSAGE_TYPE.HYPERLIQUID_REFERRAL_CONSENT; export const ASTERDEX_APPROVAL_TYPE = MESSAGE_TYPE.ASTERDEX_REFERRAL_CONSENT; export const GMX_APPROVAL_TYPE = MESSAGE_TYPE.GMX_REFERRAL_CONSENT; +export const VARIATIONAL_APPROVAL_TYPE = + MESSAGE_TYPE.VARIATIONAL_REFERRAL_CONSENT; export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = { confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation', diff --git a/shared/constants/defi-referrals.ts b/shared/constants/defi-referrals.ts index 6c003d65fa98..2c027069ab60 100644 --- a/shared/constants/defi-referrals.ts +++ b/shared/constants/defi-referrals.ts @@ -5,6 +5,7 @@ export enum DefiReferralPartner { Hyperliquid = 'hyperliquid', GMX = 'gmx', AsterDEX = 'asterdex', + Variational = 'variational', } /** @@ -32,6 +33,12 @@ export type DefiReferralPartnerConfig = { approvalType: string; /** Connection flow prior to showing the referral screen */ connectionFlow: ConnectionFlow; + /** + * When set, the referral prompt is suppressed if the user's active chain + * doesn't match this hex chain ID. This is used in cases where the partner's + * code application logic only works when on the required chain (e.g. Variational) + */ + requiredChainId?: string; }; /** @@ -70,6 +77,16 @@ export const DEFI_REFERRAL_PARTNERS: Record< approvalType: 'asterdex_referral_consent', connectionFlow: 'permissions_then_signature', }, + [DefiReferralPartner.Variational]: { + id: DefiReferralPartner.Variational, + name: 'Variational', + origin: 'https://omni.variational.io', + referralUrl: 'https://omni.variational.io?ref=OMNIMETAMASK', + learnMoreUrl: 'https://docs.variational.io/omni/rewards/referrals', + approvalType: 'variational_referral_consent', + connectionFlow: 'permissions', + requiredChainId: '0xa4b1', // Arbitrum + }, }; /** diff --git a/test/e2e/fixtures/default-fixture.json b/test/e2e/fixtures/default-fixture.json index ce4e05a3d168..2be3bb494530 100644 --- a/test/e2e/fixtures/default-fixture.json +++ b/test/e2e/fixtures/default-fixture.json @@ -1057,7 +1057,8 @@ "referrals": { "asterdex": {}, "gmx": {}, - "hyperliquid": {} + "hyperliquid": {}, + "variational": {} }, "securityAlertsEnabled": true, "showSidePanelMigrationToast": false, diff --git a/test/e2e/fixtures/onboarding-fixture.json b/test/e2e/fixtures/onboarding-fixture.json index 41cf09ec2fbb..98fa0c5f4600 100644 --- a/test/e2e/fixtures/onboarding-fixture.json +++ b/test/e2e/fixtures/onboarding-fixture.json @@ -2046,7 +2046,8 @@ "referrals": { "asterdex": {}, "gmx": {}, - "hyperliquid": {} + "hyperliquid": {}, + "variational": {} }, "securityAlertsEnabled": true, "showSidePanelMigrationToast": false, diff --git a/test/e2e/tests/settings/state-logs.json b/test/e2e/tests/settings/state-logs.json index c81eb7bab3f3..9d9b61ef7971 100644 --- a/test/e2e/tests/settings/state-logs.json +++ b/test/e2e/tests/settings/state-logs.json @@ -1298,7 +1298,8 @@ "referrals": { "asterdex": {}, "gmx": {}, - "hyperliquid": {} + "hyperliquid": {}, + "variational": {} }, "region": "string", "requests": { diff --git a/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx b/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx index e6dc6265327d..ca87a149a0e1 100644 --- a/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx +++ b/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx @@ -3,6 +3,7 @@ import { HYPERLIQUID_APPROVAL_TYPE, GMX_APPROVAL_TYPE, ASTERDEX_APPROVAL_TYPE, + VARIATIONAL_APPROVAL_TYPE, } from '../../../../../shared/constants/app'; import { DEFI_REFERRAL_PARTNERS, @@ -15,6 +16,8 @@ const HYPERLIQUID_CONFIG = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Hyperliquid]; const GMX_CONFIG = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.GMX]; const ASTERDEX_CONFIG = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.AsterDEX]; +const VARIATIONAL_CONFIG = + DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Variational]; const STATE_MOCK_DEFAULT = { metamask: { @@ -23,6 +26,7 @@ const STATE_MOCK_DEFAULT = { [DefiReferralPartner.Hyperliquid]: {}, [DefiReferralPartner.GMX]: {}, [DefiReferralPartner.AsterDEX]: {}, + [DefiReferralPartner.Variational]: {}, }, }, }, @@ -103,3 +107,22 @@ export const AsterdexStory = (args: { selectedAddress: string }) => { }; AsterdexStory.storyName = 'Asterdex'; + +export const VariationalStory = (args: { selectedAddress: string }) => { + return ( + + + + ); +}; + +VariationalStory.storyName = 'Variational'; diff --git a/ui/pages/confirmations/confirmation/templates/approval-templates.ts b/ui/pages/confirmations/confirmation/templates/approval-templates.ts index 3f785f4c1e3b..c2a2b2248ebc 100644 --- a/ui/pages/confirmations/confirmation/templates/approval-templates.ts +++ b/ui/pages/confirmations/confirmation/templates/approval-templates.ts @@ -3,6 +3,7 @@ import { HYPERLIQUID_APPROVAL_TYPE, ASTERDEX_APPROVAL_TYPE, GMX_APPROVAL_TYPE, + VARIATIONAL_APPROVAL_TYPE, SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES, } from '../../../../../shared/constants/app'; import createSnapAccount from './create-snap-account'; @@ -40,4 +41,5 @@ export const APPROVAL_TEMPLATES = { [HYPERLIQUID_APPROVAL_TYPE]: defiReferralConsent, [ASTERDEX_APPROVAL_TYPE]: defiReferralConsent, [GMX_APPROVAL_TYPE]: defiReferralConsent, + [VARIATIONAL_APPROVAL_TYPE]: defiReferralConsent, } as const; diff --git a/ui/pages/confirmations/confirmation/templates/approval-types.ts b/ui/pages/confirmations/confirmation/templates/approval-types.ts index bc0fe1cb4bc1..9e9aeecb5871 100644 --- a/ui/pages/confirmations/confirmation/templates/approval-types.ts +++ b/ui/pages/confirmations/confirmation/templates/approval-types.ts @@ -3,6 +3,7 @@ import { HYPERLIQUID_APPROVAL_TYPE, ASTERDEX_APPROVAL_TYPE, GMX_APPROVAL_TYPE, + VARIATIONAL_APPROVAL_TYPE, SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES, } from '../../../../../shared/constants/app'; import type { IsEquivalent } from '../../../../../shared/types/type-level-utils'; @@ -29,6 +30,7 @@ export const TEMPLATED_CONFIRMATION_APPROVAL_TYPES = [ HYPERLIQUID_APPROVAL_TYPE, ASTERDEX_APPROVAL_TYPE, GMX_APPROVAL_TYPE, + VARIATIONAL_APPROVAL_TYPE, ] as const; // ────────────────────────────────────────────────────────────────────────── diff --git a/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx b/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx index a2d3e4267727..897c9945b23d 100644 --- a/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx +++ b/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx @@ -53,6 +53,7 @@ export const DefiReferralConsentControl: React.FC = ({ t('hyperliquidReferralSubtitle'); t('gmxReferralSubtitle'); t('asterdexReferralSubtitle'); + t('variationalReferralSubtitle'); return (