diff --git a/packages/walletkit-ui/src/contexts/FeatureFlagProvider.test.tsx b/packages/walletkit-ui/src/contexts/FeatureFlagProvider.test.tsx
new file mode 100644
index 0000000..0adf8d9
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/FeatureFlagProvider.test.tsx
@@ -0,0 +1,80 @@
+/** @jest-environment jsdom */
+import { render, screen } from "@testing-library/react";
+import { EnvironmentNetwork } from "@waveshq/walletkit-core";
+import React, { PropsWithChildren, useEffect } from "react";
+
+import {
+ FeatureFlagProvider,
+ useFeatureFlagContext,
+} from "./FeatureFlagProvider";
+import { StoreServiceProvider } from "./StoreServiceProvider";
+
+function TestingComponent(): JSX.Element {
+ // const { hasBetaFeatures } = useFeatureFlagContext();
+ return (
+
+ {/*
{hasBetaFeatures}
*/}
+
{true}
+
+ );
+}
+const mockApi = {
+ get: jest.fn(),
+ set: jest.fn(),
+};
+const consoleLog = jest.spyOn(console, "log").mockImplementation(jest.fn);
+const consoleError = jest.spyOn(console, "error").mockImplementation(jest.fn);
+const logger = { error: () => consoleError, info: () => consoleLog };
+
+jest.mock("./NetworkContext", () => ({
+ useNetworkContext: () => ({
+ network: EnvironmentNetwork.RemotePlayground,
+ networkName: "regtest",
+ updateNetwork: jest.fn(),
+ }),
+}));
+
+// jest.mock("./StoreServiceProvider", () => ({
+// useServiceProviderContext: () => {
+// return {
+// url: "",
+// defaultUrl: "",
+// isCustomUrl: false,
+// setUrl: jest.fn(),
+// };
+// },
+// }));
+
+describe.skip("Feature flag provider", () => {
+ it.only("should match snapshot", () => {
+ const rendered = render(
+
+
+
+
+
+ );
+ expect(rendered).toMatchSnapshot();
+ });
+
+ it("should render hasBetaFeatures flag", () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId("flag").textContent).toStrictEqual("true");
+ });
+});
diff --git a/packages/walletkit-ui/src/contexts/FeatureFlagProvider.tsx b/packages/walletkit-ui/src/contexts/FeatureFlagProvider.tsx
new file mode 100644
index 0000000..99b6746
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/FeatureFlagProvider.tsx
@@ -0,0 +1,196 @@
+import {
+ FeatureFlag,
+ FeatureFlagID,
+ getEnvironment,
+ Platform,
+} from "@waveshq/walletkit-core";
+import React, {
+ createContext,
+ PropsWithChildren,
+ ReactElement,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import { satisfies } from "semver";
+
+import { useGetFeatureFlagsQuery, usePrefetch } from "../store";
+import { BaseLogger } from "./logger";
+import { useNetworkContext } from "./NetworkContext";
+import { useServiceProviderContext } from "./StoreServiceProvider";
+
+const MAX_RETRY = 3;
+export interface FeatureFlagContextI {
+ featureFlags: FeatureFlag[];
+ enabledFeatures: FeatureFlagID[];
+ updateEnabledFeatures: (features: FeatureFlagID[]) => void;
+ isFeatureAvailable: (featureId: FeatureFlagID) => boolean;
+ isBetaFeature: (featureId: FeatureFlagID) => boolean;
+ hasBetaFeatures: boolean;
+}
+
+const FeatureFlagContext = createContext(undefined as any);
+
+export function useFeatureFlagContext(): FeatureFlagContextI {
+ return useContext(FeatureFlagContext);
+}
+
+export interface FeatureFlagProviderProps extends PropsWithChildren<{}> {
+ api: {
+ get: () => Promise;
+ set: (features: FeatureFlagID[]) => Promise;
+ };
+ logger: BaseLogger;
+ releaseChannel: string;
+ platformOS: Platform;
+ nativeApplicationVersion: string | null;
+}
+
+export function FeatureFlagProvider(
+ props: FeatureFlagProviderProps
+): JSX.Element | null {
+ const {
+ logger,
+ api,
+ releaseChannel,
+ platformOS,
+ nativeApplicationVersion,
+ children,
+ } = props;
+ const { network } = useNetworkContext();
+ const { url, isCustomUrl } = useServiceProviderContext();
+ const {
+ data: featureFlags = [],
+ isLoading,
+ isError,
+ refetch,
+ } = useGetFeatureFlagsQuery(`${network}.${url}`);
+
+ const prefetchPage = usePrefetch("getFeatureFlags");
+ const appVersion = nativeApplicationVersion ?? "0.0.0";
+ const [enabledFeatures, setEnabledFeatures] = useState([]);
+ const [retries, setRetries] = useState(0);
+
+ useEffect(() => {
+ if (isError && retries < MAX_RETRY) {
+ setTimeout(() => {
+ prefetchPage({});
+ setRetries(retries + 1);
+ }, 10000);
+ } else if (!isError) {
+ prefetchPage({});
+ }
+ }, [isError]);
+
+ useEffect(() => {
+ refetch();
+ }, [network]);
+
+ function isBetaFeature(featureId: FeatureFlagID): boolean {
+ return featureFlags.some(
+ (flag: FeatureFlag) =>
+ satisfies(appVersion, flag.version) &&
+ flag.networks?.includes(network) &&
+ flag.id === featureId &&
+ flag.stage === "beta"
+ );
+ }
+
+ function checkFeatureStage(feature: FeatureFlag): boolean {
+ switch (feature.stage) {
+ case "alpha":
+ return getEnvironment(releaseChannel).debug;
+ case "beta":
+ return enabledFeatures.includes(feature.id);
+ case "public":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ function isFeatureAvailable(featureId: FeatureFlagID): boolean {
+ return featureFlags.some((flag: FeatureFlag) => {
+ if (
+ flag.networks?.includes(network) &&
+ flag.app?.includes("MOBILE_LW") &&
+ flag.platforms?.includes(platformOS)
+ ) {
+ if (platformOS === "web") {
+ return flag.id === featureId && checkFeatureStage(flag);
+ }
+ return (
+ satisfies(appVersion, flag.version) &&
+ flag.id === featureId &&
+ checkFeatureStage(flag)
+ );
+ }
+ return false;
+ });
+ }
+
+ const updateEnabledFeatures = async (
+ flags: FeatureFlagID[]
+ ): Promise => {
+ setEnabledFeatures(flags);
+ await api.set(flags);
+ };
+
+ useEffect(() => {
+ api
+ .get()
+ .then((features) => {
+ setEnabledFeatures(features);
+ })
+ .catch((err) => logger.error(err));
+ }, []);
+
+ const context: FeatureFlagContextI = useMemo(
+ () => ({
+ featureFlags,
+ enabledFeatures,
+ updateEnabledFeatures,
+ isFeatureAvailable,
+ isBetaFeature,
+ hasBetaFeatures: featureFlags.some(
+ (flag) =>
+ satisfies(appVersion, flag.version) &&
+ flag.networks?.includes(network) &&
+ flag.platforms?.includes(platformOS) &&
+ flag.stage === "beta"
+ ),
+ }),
+ [featureFlags, appVersion, network, platformOS]
+ );
+
+ /*
+ If service provider === custom, we keep showing the app regardless if feature flags loaded to ensure app won't be stuck on white screen
+ Note: return null === app will be stuck at white screen until the feature flags API are applied
+ */
+ if (isLoading && !isCustomUrl) {
+ return null;
+ }
+
+ if (isError && !isLoading && retries < MAX_RETRY) {
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>>;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function FeatureGate({
+ children,
+ feature,
+}: {
+ children: ReactElement;
+ feature: FeatureFlagID;
+}): JSX.Element | null {
+ const { isFeatureAvailable } = useFeatureFlagContext();
+ return isFeatureAvailable(feature) ? children : null;
+}
diff --git a/packages/walletkit-ui/src/contexts/__snapshots__/FeatureFlagProvider.test.tsx.snap b/packages/walletkit-ui/src/contexts/__snapshots__/FeatureFlagProvider.test.tsx.snap
new file mode 100644
index 0000000..4577186
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/__snapshots__/FeatureFlagProvider.test.tsx.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Feature flag provider should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+ ,
+ "container": ,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;