diff --git a/.changeset/friendly-doors-ring.md b/.changeset/friendly-doors-ring.md new file mode 100644 index 000000000000..2b264c1cb5aa --- /dev/null +++ b/.changeset/friendly-doors-ring.md @@ -0,0 +1,7 @@ +--- +"@ledgerhq/types-live": patch +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +--- + +add ptxSwapCoreExperiment diff --git a/apps/ledger-live-desktop/src/renderer/analytics/segment.ts b/apps/ledger-live-desktop/src/renderer/analytics/segment.ts index 4aff4b9b914d..73a8e59d1b63 100644 --- a/apps/ledger-live-desktop/src/renderer/analytics/segment.ts +++ b/apps/ledger-live-desktop/src/renderer/analytics/segment.ts @@ -1,36 +1,36 @@ -import { v4 as uuid } from "uuid"; +import { runOnceWhen } from "@ledgerhq/live-common/utils/runOnceWhen"; +import { getEnv } from "@ledgerhq/live-env"; +import { + GENESIS_PASS_COLLECTION_CONTRACT, + hasNftInAccounts, + INFINITY_PASS_COLLECTION_CONTRACT, +} from "@ledgerhq/live-nft"; +import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName"; +import { AccountLike, Feature, FeatureId, Features, idsToLanguage } from "@ledgerhq/types-live"; import invariant from "invariant"; +import { useCallback, useContext } from "react"; import { ReplaySubject } from "rxjs"; -import { getEnv } from "@ledgerhq/live-env"; -import logger from "~/renderer/logger"; +import { v4 as uuid } from "uuid"; import { getParsedSystemLocale } from "~/helpers/systemLocale"; import user from "~/helpers/user"; -import { runOnceWhen } from "@ledgerhq/live-common/utils/runOnceWhen"; +import logger from "~/renderer/logger"; +import { State } from "~/renderer/reducers"; import { - sidebarCollapsedSelector, - shareAnalyticsSelector, + developerModeSelector, + devicesModelListSelector, + hasSeenAnalyticsOptInPromptSelector, + languageSelector, lastSeenDeviceSelector, localeSelector, - languageSelector, - devicesModelListSelector, + shareAnalyticsSelector, sharePersonalizedRecommendationsSelector, - hasSeenAnalyticsOptInPromptSelector, + sidebarCollapsedSelector, trackingEnabledSelector, - developerModeSelector, } from "~/renderer/reducers/settings"; -import { State } from "~/renderer/reducers"; -import { AccountLike, Feature, FeatureId, Features, idsToLanguage } from "@ledgerhq/types-live"; -import { accountsSelector } from "../reducers/accounts"; -import { - GENESIS_PASS_COLLECTION_CONTRACT, - hasNftInAccounts, - INFINITY_PASS_COLLECTION_CONTRACT, -} from "@ledgerhq/live-nft"; import createStore from "../createStore"; -import { currentRouteNameRef, previousRouteNameRef } from "./screenRefs"; -import { useCallback, useContext } from "react"; import { analyticsDrawerContext } from "../drawers/Provider"; -import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName"; +import { accountsSelector } from "../reducers/accounts"; +import { currentRouteNameRef, previousRouteNameRef } from "./screenRefs"; invariant(typeof window !== "undefined", "analytics/segment must be called on renderer thread"); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -89,6 +89,9 @@ const getPtxAttributes = () => { const ptxSwapLiveAppDemoThree = analyticsFeatureFlagMethod("ptxSwapLiveAppDemoThree")?.enabled; const ptxSwapThorswapProvider = analyticsFeatureFlagMethod("ptxSwapThorswapProvider")?.enabled; const ptxSwapExodusProvider = analyticsFeatureFlagMethod("ptxSwapExodusProvider")?.enabled; + const ptxSwapCoreExperimentFlag = analyticsFeatureFlagMethod("ptxSwapCoreExperiment"); + const ptxSwapCoreExperiment = + ptxSwapCoreExperimentFlag?.enabled && ptxSwapCoreExperimentFlag?.params?.variant; const isBatch1Enabled: boolean = !!fetchAdditionalCoins?.enabled && fetchAdditionalCoins?.params?.batch === 1; @@ -115,6 +118,7 @@ const getPtxAttributes = () => { ptxSwapLiveAppDemoThree, ptxSwapThorswapProvider, ptxSwapExodusProvider, + ptxSwapCoreExperiment, }; }; diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebView.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebView.tsx index f1b838d799a0..2171af62be8b 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebView.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebView.tsx @@ -35,6 +35,7 @@ import { NetworkDown } from "@ledgerhq/errors"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/impl"; import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index"; import { SwapExchangeRateAmountTooLow } from "@ledgerhq/live-common/errors"; +import { useSwapLiveConfig } from "@ledgerhq/live-common/exchange/swap/hooks/index"; import { getAbandonSeedAddress } from "@ledgerhq/live-common/exchange/swap/hooks/useFromState"; import { SwapLiveError } from "@ledgerhq/live-common/exchange/swap/types"; import { @@ -136,6 +137,7 @@ const SwapWebView = ({ const { networkStatus } = useNetworkStatus(); const isOffline = networkStatus === NetworkStatus.OFFLINE; const swapDefaultTrack = useGetSwapTrackingProperties(); + const swapLiveEnabledFlag = useSwapLiveConfig(); const hasSwapState = !!swapState; const customPTXHandlers = usePTXCustomHandlers(manifest); @@ -471,6 +473,11 @@ const SwapWebView = ({ toNewTokenId: swapState?.toNewTokenId, feeStrategy: swapState?.feeStrategy, customFeeConfig: swapState?.customFeeConfig, + ...(swapLiveEnabledFlag?.params && + "variant" in swapLiveEnabledFlag.params && + swapLiveEnabledFlag.params.variant === "Demo0" + ? { ptxSwapCoreExperiment: "Demo0" } + : {}), }; Object.entries(swapParams).forEach(([key, value]) => { @@ -489,6 +496,7 @@ const SwapWebView = ({ isMaxEnabled, fromCurrency, targetCurrency?.id, + swapLiveEnabledFlag, ]); useEffect(() => { diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebViewDemo3.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebViewDemo3.tsx index 14e3ce73afd2..875b1632e62c 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebViewDemo3.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/SwapWebViewDemo3.tsx @@ -33,6 +33,7 @@ import WebviewErrorDrawer from "./WebviewErrorDrawer/index"; import { GasOptions } from "@ledgerhq/coin-evm/lib/types/transaction"; import { getMainAccount, getParentAccount } from "@ledgerhq/live-common/account/helpers"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/impl"; +import { useSwapLiveConfig } from "@ledgerhq/live-common/exchange/swap/hooks/index"; import { getAbandonSeedAddress } from "@ledgerhq/live-common/exchange/swap/hooks/useFromState"; import { convertToAtomicUnit, @@ -123,6 +124,7 @@ const SwapWebView = ({ manifest, liveAppUnavailable }: SwapWebProps) => { from?: string; }>(); const redirectToHistory = useRedirectToSwapHistory(); + const swapLiveEnabledFlag = useSwapLiveConfig(); const { networkStatus } = useNetworkStatus(); const isOffline = networkStatus === NetworkStatus.OFFLINE; @@ -292,9 +294,21 @@ const SwapWebView = ({ manifest, liveAppUnavailable }: SwapWebProps) => { ).id, } : {}), - ...(state?.from ? { fromPath: simplifyFromPath(state.from) } : {}), + ...(state?.from ? { fromPath: simplifyFromPath(state?.from) } : {}), + ...(swapLiveEnabledFlag?.params && "variant" in swapLiveEnabledFlag.params + ? { + ptxSwapCoreExperiment: swapLiveEnabledFlag.params?.variant as string, + } + : {}), }).toString(), - [isOffline, state?.defaultAccount, state?.defaultParentAccount, walletState, state?.from], + [ + isOffline, + state?.defaultAccount, + state?.defaultParentAccount, + walletState, + state?.from, + swapLiveEnabledFlag, + ], ); const onSwapWebviewError = (error?: SwapLiveError) => { diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/hooks/useIsSwapLiveFlagEnabled.ts b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/hooks/useIsSwapLiveFlagEnabled.ts index 9ae7f7d4f48b..b427ffcce66f 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/hooks/useIsSwapLiveFlagEnabled.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/hooks/useIsSwapLiveFlagEnabled.ts @@ -3,20 +3,23 @@ import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; // used to get the value of the Swap Live App flag export const useIsSwapLiveFlagEnabled = (flag: string): boolean => { const demoZero = useFeature("ptxSwapLiveAppDemoZero"); - const demoOne = useFeature("ptxSwapLiveAppDemoOne"); const demoThree = useFeature("ptxSwapLiveAppDemoThree"); + const coreExperiment = useFeature("ptxSwapCoreExperiment"); + const coreExperimentVariant = coreExperiment?.enabled && coreExperiment?.params?.variant; if (flag === "ptxSwapLiveAppDemoThree") { - return !!demoThree?.enabled; + return ( + !!demoThree?.enabled || ["Demo3", "Demo3Thorswap"].includes(coreExperimentVariant as string) + ); } - if (flag === "ptxSwapLiveAppDemoOne") { - return !!demoOne?.enabled; + if (flag === "ptxSwapLiveAppDemoZero") { + return !!demoZero?.enabled || coreExperimentVariant === "Demo0"; } - if (flag === "ptxSwapLiveAppDemoZero") { - return !!demoZero?.enabled; + if (flag === "ptxSwapLiveAppDemoOne") { + return false; } - throw new Error(`Unknown Swap Live App flag: ${flag}`); + throw new Error(`Unknown Swap Live App flag ${flag}`); }; diff --git a/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.test.ts b/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.test.ts index b2357beb294c..f67fe374f076 100644 --- a/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.test.ts +++ b/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.test.ts @@ -14,12 +14,15 @@ const useMockFeature = useFeature as jest.Mock; describe("useSwapLiveConfig", () => { // Setup the mock for useFeatureFlags to return an object with getFeature const setupFeatureFlagsMock = ( - flags: Partial<{ enabled: boolean; params: { manifest_id: string } }>[], + flags: Partial< + { enabled: boolean; params: { manifest_id: string; variant?: string } } | undefined + >[], ) => { const flagsKeys = [ "ptxSwapLiveAppDemoZero", "ptxSwapLiveAppDemoOne", "ptxSwapLiveAppDemoThree", + "ptxSwapCoreExperiment", ]; useMockFeature.mockImplementation(flagName => flags[flagsKeys.indexOf(flagName)] ?? null); @@ -40,7 +43,7 @@ describe("useSwapLiveConfig", () => { expect(result.current).not.toBeNull(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.current?.params as any)?.manifest_id).toEqual("demo_3"); + expect((result.current?.params as any)?.manifest_id).toEqual("demo_0"); }); it("should return null if both features are disabled", () => { @@ -97,4 +100,99 @@ describe("useSwapLiveConfig", () => { params: { manifest_id: "swap-live-app-demo-3" }, }); }); + + it("should return config when ptxSwapCoreExperiment has valid variant Demo0", () => { + const expected = { + enabled: true, + params: { manifest_id: "swap-live-app-demo-0", variant: "Demo0" }, + }; + setupFeatureFlagsMock([{ enabled: false }, { enabled: false }, { enabled: false }, expected]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual(expected); + }); + + it("should return config when ptxSwapCoreExperiment has valid variant Demo3", () => { + setupFeatureFlagsMock([ + { enabled: false }, + { enabled: false }, + { enabled: false }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3", variant: "Demo3" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual({ + enabled: true, + params: { manifest_id: "swap-live-app-demo-3", variant: "Demo3" }, + }); + }); + + it("should return config when ptxSwapCoreExperiment has valid variant Demo3Thorswap", () => { + setupFeatureFlagsMock([ + { enabled: false }, + { enabled: false }, + { enabled: false }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3", variant: "Demo3Thorswap" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual({ + enabled: true, + params: { manifest_id: "swap-live-app-demo-3", variant: "Demo3Thorswap" }, + }); + }); + + it("should return demoZero if demoThree and are enabled", () => { + setupFeatureFlagsMock([ + { enabled: true, params: { manifest_id: "swap-live-app-demo-0" } }, + { enabled: false }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual({ + enabled: true, + params: { manifest_id: "swap-live-app-demo-0" }, + }); + }); + + it("should return demoZero if all demo flags are enabled", () => { + setupFeatureFlagsMock([ + { enabled: true, params: { manifest_id: "swap-live-app-demo-0" } }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-1" } }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual({ + enabled: true, + params: { manifest_id: "swap-live-app-demo-0" }, + }); + }); + + it("should prioritize demoZero over demo flags if all are enabled", () => { + setupFeatureFlagsMock([ + { enabled: true, params: { manifest_id: "swap-live-app-demo-0" } }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-1" } }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3" } }, + { enabled: true, params: { manifest_id: "swap-live-app-demo-3", variant: "Demo3" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toEqual({ + enabled: true, + params: { manifest_id: "swap-live-app-demo-0" }, + }); + }); + + it("should return null when coreExperiment is enabled but has no variant", () => { + setupFeatureFlagsMock([ + { enabled: false }, + { enabled: false }, + { enabled: false }, + { enabled: true, params: { manifest_id: "core-experiment" } }, + ]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toBeNull(); + }); + + it("should return null when all flags are undefined", () => { + setupFeatureFlagsMock([undefined, undefined, undefined, undefined]); + const { result } = renderHook(() => useSwapLiveConfig()); + expect(result.current).toBeNull(); + }); }); diff --git a/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.ts b/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.ts index 6cf9ec52c24f..67f50b6ccd4f 100644 --- a/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.ts +++ b/libs/ledger-live-common/src/exchange/swap/hooks/live-app-migration/useSwapLiveConfig.ts @@ -1,14 +1,36 @@ +import { Feature_PtxSwapCoreExperiment } from "@ledgerhq/types-live"; import { useFeature } from "../../../../featureFlags"; +// check if the variant is valid for core rollout experiment +export type CoreExperimentParams = NonNullable; +export type ValidVariant = CoreExperimentParams["variant"]; + // used to enable the Swap Live App globally export function useSwapLiveConfig() { - const demoZero = useFeature("ptxSwapLiveAppDemoZero"); - const demoOne = useFeature("ptxSwapLiveAppDemoOne"); const demoThree = useFeature("ptxSwapLiveAppDemoThree"); + const demoOne = useFeature("ptxSwapLiveAppDemoOne"); + const demoZero = useFeature("ptxSwapLiveAppDemoZero"); + const coreExperiment = useFeature("ptxSwapCoreExperiment"); + + if (demoZero?.enabled) { + return demoZero; + } + + if (demoThree?.enabled) { + return demoThree; + } + + if (coreExperiment?.enabled) { + const variant = coreExperiment?.params?.variant; + if (!variant || !(variant satisfies ValidVariant)) { + return null; + } + return coreExperiment; + } - // Order is important in order to get the first enabled flag - const flags = [demoThree, demoOne, demoZero]; - const enabledFlag = flags.find(flag => flag?.enabled); + if (demoOne?.enabled) { + return demoOne; + } - return enabledFlag ?? null; + return null; } diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index c43252d7582c..690541d00329 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -6,8 +6,8 @@ import { Features, } from "@ledgerhq/types-live"; import reduce from "lodash/reduce"; -import { formatToFirebaseFeatureId } from "./firebaseFeatureFlags"; import { BUY_SELL_UI_APP_ID } from "../wallet-api/constants"; +import { formatToFirebaseFeatureId } from "./firebaseFeatureFlags"; /** * Default disabled feature. @@ -416,6 +416,14 @@ export const DEFAULT_FEATURES: Features = { }, }, + ptxSwapCoreExperiment: { + enabled: false, + params: { + variant: "Demo0", + manifest_id: "swap-live-app-demo-0", + }, + }, + ptxSwapMoonpayProvider: DEFAULT_FEATURE, ptxSwapExodusProvider: DEFAULT_FEATURE, ptxSwapThorswapProvider: DEFAULT_FEATURE, diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index b41594f15f41..c7a6bf36978b 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -168,6 +168,7 @@ export type Features = CurrencyFeatures & { ptxSwapLiveAppDemoZero: Feature_PtxSwapLiveAppDemoZero; ptxSwapLiveAppDemoOne: Feature_PtxSwapLiveAppDemoZero; ptxSwapLiveAppDemoThree: Feature_PtxSwapLiveAppDemoZero; + ptxSwapCoreExperiment: Feature_PtxSwapCoreExperiment; ptxSwapMoonpayProvider: Feature_PtxSwapMoonpayProvider; ptxSwapExodusProvider: Feature_PtxSwapExodusProvider; ptxSwapThorswapProvider: Feature_PtxSwapThorswapProvider; @@ -468,6 +469,13 @@ export type Feature_PtxSwapLiveAppDemoZero = Feature<{ families?: string[]; }>; +export type Feature_PtxSwapCoreExperiment = Feature<{ + variant: "Demo0" | "Demo3" | "Demo3Thorswap"; + manifest_id: string; + currencies?: string[]; + families?: string[]; +}>; + export type Feature_FetchAdditionalCoins = Feature<{ batch: number; }>;