Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(live-14205): add ptxSwapCoreExperiment flag 2 #8053

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/friendly-doors-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ledgerhq/types-live": patch
"ledger-live-desktop": patch
"@ledgerhq/live-common": patch
---

add ptxSwapCoreExperiment
46 changes: 25 additions & 21 deletions apps/ledger-live-desktop/src/renderer/analytics/segment.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -115,6 +118,7 @@ const getPtxAttributes = () => {
ptxSwapLiveAppDemoThree,
ptxSwapThorswapProvider,
ptxSwapExodusProvider,
ptxSwapCoreExperiment,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]) => {
Expand All @@ -489,6 +496,7 @@ const SwapWebView = ({
isMaxEnabled,
fromCurrency,
targetCurrency?.id,
swapLiveEnabledFlag,
]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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<Feature_PtxSwapCoreExperiment["params"]>;
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;
}
10 changes: 9 additions & 1 deletion libs/ledger-live-common/src/featureFlags/defaultFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading