Feature flags strategies (#3800)
* Feature flags * Feature flags * Feature flags * Feature flags * Feature flags * Feature flags * Types * use @swc/jest * Avoid calling constructors inside * Types * Types * remove flagsmith * Change to payload * Change to payload * Update tests * Split resolver
This commit is contained in:
parent
f4e6ab4101
commit
3118741db8
46 changed files with 567 additions and 348 deletions
|
@ -8,7 +8,6 @@ import { BrowserRouter } from "react-router-dom";
|
|||
import { ExternalAppProvider } from "../../src/apps/components/ExternalAppContext";
|
||||
import { DevModeProvider } from "../../src/components/DevModePanel/DevModeProvider";
|
||||
import { Locale, RawLocaleProvider } from "../../src/components/Locale";
|
||||
import { FlagsServiceProvider } from "../../src/hooks/useFlags/flagsService";
|
||||
import { paletteOverrides, themeOverrides } from "../../src/themeOverrides";
|
||||
|
||||
import { Provider as DateProvider } from "../../src/components/Date/DateContext";
|
||||
|
@ -16,6 +15,7 @@ import { TimezoneProvider } from "../../src/components/Timezone";
|
|||
import MessageManagerProvider from "../../src/components/messages";
|
||||
import { getAppMountUri } from "../../src/config";
|
||||
import { ApolloMockedProvider } from "../../testUtils/ApolloMockedProvider";
|
||||
import { FeatureFlagsProvider } from "@dashboard/featureFlags";
|
||||
|
||||
export const MockedProvidersDecorator: React.FC = ({ children }) => (
|
||||
<ApolloMockedProvider>
|
||||
|
@ -35,7 +35,7 @@ export const MockedProvidersDecorator: React.FC = ({ children }) => (
|
|||
<ThemeProvider>
|
||||
<BrowserRouter basename={getAppMountUri()}>
|
||||
<ExternalAppProvider>
|
||||
<FlagsServiceProvider>
|
||||
<FeatureFlagsProvider strategies={[]}>
|
||||
<MessageManagerProvider>
|
||||
<DevModeProvider>
|
||||
<div
|
||||
|
@ -47,7 +47,7 @@ export const MockedProvidersDecorator: React.FC = ({ children }) => (
|
|||
</div>
|
||||
</DevModeProvider>
|
||||
</MessageManagerProvider>
|
||||
</FlagsServiceProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</ExternalAppProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -21,9 +21,3 @@ Create or edit `.env` file in a root directory or set environment variables with
|
|||
- `APPS_MARKETPLACE_API_URI` - URI of Marketplace API to fetch list of Apps in JSON.
|
||||
|
||||
- `APPS_TUNNEL_URL_KEYWORDS` - Custom apps tunnel URL keywords.
|
||||
|
||||
- `FLAGS_SERVICE_ENABLED` - Boolean flag determines whether we use external feature flags provider.
|
||||
If you set `FLAGS_SERVICE_ENABLED` to "true", we'll be using external feature flag provider as source or flags.
|
||||
If you set `FLAGS_SERVICE_ENABLED` to "false" or not set, we'll use fallback flags from environment variables.
|
||||
|
||||
- `FLAGSMITH_ID` - Flagsmith environment id
|
||||
|
|
60
package-lock.json
generated
60
package-lock.json
generated
|
@ -118,6 +118,7 @@
|
|||
"@percy/cli": "^1.21.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@saleor/app-sdk": "0.39.1",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/color-convert": "^2.0.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
@ -5501,6 +5502,18 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/create-cache-key-function": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz",
|
||||
"integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment": {
|
||||
"version": "27.5.1",
|
||||
"license": "MIT",
|
||||
|
@ -11034,6 +11047,22 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/jest": {
|
||||
"version": "0.2.26",
|
||||
"resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.26.tgz",
|
||||
"integrity": "sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^27.4.2",
|
||||
"jsonc-parser": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.5",
|
||||
"license": "MIT",
|
||||
|
@ -24831,6 +24860,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"devOptional": true,
|
||||
|
@ -38981,6 +39016,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@jest/create-cache-key-function": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz",
|
||||
"integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1"
|
||||
}
|
||||
},
|
||||
"@jest/environment": {
|
||||
"version": "27.5.1",
|
||||
"optional": true,
|
||||
|
@ -42885,6 +42929,16 @@
|
|||
"integrity": "sha512-73DGsjsJYSzmoRbfomPj5jcQawtK2H0bCDi/1wgfl8NKVOuzrq+PpaTry3lzx+gvTHxUX6mUHV22i7C9ITL74Q==",
|
||||
"optional": true
|
||||
},
|
||||
"@swc/jest": {
|
||||
"version": "0.2.26",
|
||||
"resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.26.tgz",
|
||||
"integrity": "sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/create-cache-key-function": "^27.4.2",
|
||||
"jsonc-parser": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "4.0.5",
|
||||
"optional": true,
|
||||
|
@ -52246,6 +52300,12 @@
|
|||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"devOptional": true,
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
"@percy/cli": "^1.21.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@saleor/app-sdk": "0.39.1",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/color-convert": "^2.0.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
@ -253,7 +254,7 @@
|
|||
],
|
||||
"testEnvironment": "jest-environment-jsdom",
|
||||
"transform": {
|
||||
"^.+\\.(jsx?|tsx?)$": "babel-jest",
|
||||
"^.+\\.(jsx?|tsx?)$": "@swc/jest",
|
||||
"^.+\\.(png|svg|jpe?g)$": "jest-file"
|
||||
},
|
||||
"testRegex": ".*\\.test\\.tsx?$",
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
// @ts-strict-ignore
|
||||
import { useAppDashboardUpdates } from "@dashboard/apps/components/AppFrame/useAppDashboardUpdates";
|
||||
import { useUpdateAppToken } from "@dashboard/apps/components/AppFrame/useUpdateAppToken";
|
||||
import {
|
||||
AppDetailsUrlQueryParams,
|
||||
prepareFeatureFlagsList,
|
||||
} from "@dashboard/apps/urls";
|
||||
import { useAllFlags } from "@dashboard/hooks/useFlags";
|
||||
import { AppDetailsUrlQueryParams } from "@dashboard/apps/urls";
|
||||
import { useAllFlags } from "@dashboard/featureFlags";
|
||||
import { CircularProgress } from "@material-ui/core";
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import { AppIFrame } from "./AppIFrame";
|
||||
import { useStyles } from "./styles";
|
||||
|
@ -76,8 +73,6 @@ export const AppFrame: React.FC<Props> = ({
|
|||
setHandshakeDone(true);
|
||||
}, [appToken, postToExtension, setHandshakeDone]);
|
||||
|
||||
const featureFlags = useMemo(() => prepareFeatureFlagsList(flags), [flags]);
|
||||
|
||||
useUpdateAppToken({
|
||||
postToExtension,
|
||||
appToken,
|
||||
|
@ -98,7 +93,7 @@ export const AppFrame: React.FC<Props> = ({
|
|||
ref={frameRef}
|
||||
src={src}
|
||||
appId={appId}
|
||||
featureFlags={featureFlags}
|
||||
featureFlags={flags}
|
||||
params={params}
|
||||
onLoad={handleLoad}
|
||||
onError={onError}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @ts-strict-ignore
|
||||
import { AppDetailsUrlQueryParams, AppUrls } from "@dashboard/apps/urls";
|
||||
import { FlagList } from "@dashboard/featureFlags";
|
||||
import { ThemeType } from "@saleor/app-sdk/app-bridge";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
import isEqualWith from "lodash/isEqualWith";
|
||||
|
@ -8,7 +9,7 @@ import React, { forwardRef, memo, useEffect, useRef } from "react";
|
|||
interface AppIFrameProps {
|
||||
appId: string;
|
||||
src: string;
|
||||
featureFlags: Record<string, string>;
|
||||
featureFlags: FlagList;
|
||||
params: AppDetailsUrlQueryParams;
|
||||
onLoad: () => void;
|
||||
onError: () => void;
|
||||
|
|
|
@ -7,6 +7,27 @@ import { renderHook } from "@testing-library/react-hooks";
|
|||
import * as ReactIntl from "react-intl";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
jest.mock("@dashboard/config", () => {
|
||||
const actualModule = jest.requireActual("@dashboard/config");
|
||||
return {
|
||||
__esModule: true,
|
||||
...actualModule,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
"@dashboard/apps/components/ExternalAppContext/ExternalAppContext",
|
||||
() => {
|
||||
const actualModule = jest.requireActual(
|
||||
"@dashboard/apps/components/ExternalAppContext/ExternalAppContext",
|
||||
);
|
||||
return {
|
||||
__esModule: true,
|
||||
...actualModule,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const mockNotify = jest.fn();
|
||||
const mockCloseExternalApp = jest.fn();
|
||||
|
||||
|
|
|
@ -2,6 +2,14 @@ import { AppUrls } from "@dashboard/apps/urls";
|
|||
import * as config from "@dashboard/config";
|
||||
import { ThemeType } from "@saleor/app-sdk/app-bridge";
|
||||
|
||||
jest.mock("@dashboard/config", () => {
|
||||
const actualModule = jest.requireActual("@dashboard/config");
|
||||
return {
|
||||
__esModule: true,
|
||||
...actualModule,
|
||||
};
|
||||
});
|
||||
|
||||
describe("AppUrls (apps/urls.ts)", () => {
|
||||
describe("isAppDeepUrlChange", () => {
|
||||
it("Returns true if only nested app path changes", () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getApiUrl } from "@dashboard/config";
|
||||
import { FlagWithName } from "@dashboard/hooks/useFlags/types";
|
||||
import { FlagList } from "@dashboard/featureFlags";
|
||||
import { stringifyQs } from "@dashboard/utils/urls";
|
||||
import { ThemeType } from "@saleor/app-sdk/app-bridge";
|
||||
import urlJoin from "url-join";
|
||||
|
@ -24,7 +24,7 @@ export interface AppDetailsUrlMountQueryParams {
|
|||
}
|
||||
|
||||
interface FeatureFlagsQueryParams {
|
||||
featureFlags?: Record<string, string>;
|
||||
featureFlags?: FlagList;
|
||||
}
|
||||
export interface AppDetailsCommonParams {
|
||||
theme: ThemeType;
|
||||
|
@ -36,16 +36,6 @@ export type AppDetailsUrlQueryParams = Dialog<AppDetailsUrlDialog> &
|
|||
|
||||
export type AppInstallUrlQueryParams = Partial<{ [MANIFEST_ATTR]: string }>;
|
||||
|
||||
export const prepareFeatureFlagsList = (
|
||||
flags: FlagWithName[],
|
||||
): Record<string, string> =>
|
||||
flags.reduce<Record<string, string>>((acc, flag) => {
|
||||
if (flag.enabled) {
|
||||
acc[flag.name] = `${flag.value || true}`;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const AppSections = {
|
||||
appsSection: "/apps/",
|
||||
};
|
||||
|
|
35
src/featureFlags/FeatureFlagsProvider.tsx
Normal file
35
src/featureFlags/FeatureFlagsProvider.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import LoginLoading from "@dashboard/auth/components/LoginLoading/LoginLoading";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { FlagList } from "./availableFlags";
|
||||
import { Provider } from "./context";
|
||||
import { useFlagsResolver } from "./FlagsResolver";
|
||||
import { AvailableStrategies } from "./strategies";
|
||||
|
||||
interface FeatureFlagsProviderProps {
|
||||
children: ReactNode;
|
||||
strategies: AvailableStrategies[];
|
||||
}
|
||||
|
||||
export const FeatureFlagsProvider = ({
|
||||
children,
|
||||
strategies,
|
||||
}: FeatureFlagsProviderProps) => {
|
||||
const resolver = useFlagsResolver(strategies);
|
||||
const [flags, setFlags] = useState<FlagList | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const disableLoading = () => setLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
resolver
|
||||
.fetchAll()
|
||||
.combineWithPriorities()
|
||||
.then(setFlags)
|
||||
.finally(disableLoading);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider value={flags}>{loading ? <LoginLoading /> : children}</Provider>
|
||||
);
|
||||
};
|
31
src/featureFlags/FlagContent.test.ts
Normal file
31
src/featureFlags/FlagContent.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { FlagValue } from "./FlagContent";
|
||||
|
||||
describe("featureFlags/FlagValue", () => {
|
||||
it("creates from string", async () => {
|
||||
// Arrange
|
||||
const stringValue = `{ "enabled": true, "payload": "test" }`;
|
||||
|
||||
// Act
|
||||
const instance = FlagValue.fromString(stringValue);
|
||||
|
||||
// Assert
|
||||
expect(instance).toEqual({ enabled: true, payload: "test" });
|
||||
});
|
||||
|
||||
it("creates empty", async () => {
|
||||
// Act
|
||||
const instance = FlagValue.empty();
|
||||
|
||||
// Arrange
|
||||
expect(instance).toEqual({ enabled: false, payload: "" });
|
||||
});
|
||||
|
||||
it("returns a JSON string", async () => {
|
||||
// Arrange
|
||||
const instance = new FlagValue(true, "some value");
|
||||
|
||||
const jsonString = instance.asString();
|
||||
|
||||
expect(jsonString).toEqual('{"enabled":true,"payload":"some value"}');
|
||||
});
|
||||
});
|
21
src/featureFlags/FlagContent.ts
Normal file
21
src/featureFlags/FlagContent.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class FlagValue {
|
||||
constructor(public enabled: boolean, public payload?: string) {}
|
||||
|
||||
public static fromString(value: string) {
|
||||
try {
|
||||
const { enabled, payload } = JSON.parse(value);
|
||||
|
||||
return new FlagValue(enabled, payload);
|
||||
} catch (e) {
|
||||
return FlagValue.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static empty() {
|
||||
return new FlagValue(false, "");
|
||||
}
|
||||
|
||||
public asString() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
102
src/featureFlags/FlagsResolver.test.ts
Normal file
102
src/featureFlags/FlagsResolver.test.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { FlagsResolver } from "./FlagsResolver";
|
||||
import { Strategy } from "./Strategy";
|
||||
|
||||
jest.mock("./availableFlags", () => ({
|
||||
isSupported: () => true,
|
||||
}));
|
||||
|
||||
describe("featureFlags/FlagsResolver", () => {
|
||||
it("fetches flags data in given order", async () => {
|
||||
// Arrange
|
||||
const strategy1 = {
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test1" },
|
||||
}),
|
||||
} as unknown as Strategy;
|
||||
|
||||
const strategy2 = {
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test2" },
|
||||
}),
|
||||
} as unknown as Strategy;
|
||||
|
||||
const defaultStrategy = {
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "default" },
|
||||
}),
|
||||
} as unknown as Strategy;
|
||||
|
||||
const resolver = new FlagsResolver([strategy1, strategy2], defaultStrategy);
|
||||
|
||||
// Act
|
||||
const results = await resolver.fetchAll().getResult();
|
||||
|
||||
// Assert
|
||||
expect(results).toEqual([
|
||||
{ flag1: { enabled: true, payload: "test1" } },
|
||||
{ flag1: { enabled: true, payload: "test2" } },
|
||||
{ flag1: { enabled: true, payload: "default" } },
|
||||
]);
|
||||
});
|
||||
|
||||
// Arrange
|
||||
it.each([
|
||||
{
|
||||
strategies: [
|
||||
{
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test1" },
|
||||
}),
|
||||
} as unknown as Strategy,
|
||||
],
|
||||
expected: {
|
||||
flag1: { enabled: true, payload: "test1" },
|
||||
flagD: { enabled: true, payload: "some default" },
|
||||
},
|
||||
},
|
||||
{
|
||||
strategies: [
|
||||
{
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test1-a" },
|
||||
}),
|
||||
} as unknown as Strategy,
|
||||
{
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test1-b" },
|
||||
flag2: { enabled: true, payload: "test2-b" },
|
||||
}),
|
||||
} as unknown as Strategy,
|
||||
{
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "test1-c" },
|
||||
flag2: { enabled: true, payload: "test2-c" },
|
||||
}),
|
||||
} as unknown as Strategy,
|
||||
],
|
||||
expected: {
|
||||
flag1: { enabled: true, payload: "test1-a" },
|
||||
flag2: { enabled: true, payload: "test2-b" },
|
||||
flagD: { enabled: true, payload: "some default" },
|
||||
},
|
||||
},
|
||||
])(
|
||||
"combines flags acorrding to the order",
|
||||
async ({ strategies, expected }) => {
|
||||
const defaultStrategy = {
|
||||
fetchAll: () => ({
|
||||
flag1: { enabled: true, payload: "default" },
|
||||
flagD: { enabled: true, payload: "some default" },
|
||||
}),
|
||||
} as unknown as Strategy;
|
||||
|
||||
const resolver = new FlagsResolver(strategies, defaultStrategy);
|
||||
|
||||
// Act
|
||||
const results = await resolver.fetchAll().combineWithPriorities();
|
||||
|
||||
// Assert
|
||||
expect(results).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
43
src/featureFlags/FlagsResolver/index.ts
Normal file
43
src/featureFlags/FlagsResolver/index.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { useRef } from "react";
|
||||
|
||||
import * as AvailableFlags from "./../availableFlags";
|
||||
import { AvailableStrategies } from "./../strategies";
|
||||
import { DefaultsStrategy } from "./../strategies/DefaultsStrategy";
|
||||
import { Strategy } from "./../Strategy";
|
||||
import { reduceFlagListArray } from "./reduceFlagListArray";
|
||||
|
||||
export class FlagsResolver {
|
||||
private results: Promise<AvailableFlags.FlagList[]>;
|
||||
private strategies: Strategy[];
|
||||
|
||||
constructor(
|
||||
strategies: Strategy[],
|
||||
defaultStrategy = new DefaultsStrategy(),
|
||||
) {
|
||||
this.results = Promise.resolve([]);
|
||||
this.strategies = [...strategies, defaultStrategy];
|
||||
}
|
||||
|
||||
public fetchAll() {
|
||||
const promises = this.strategies.map(s => s.fetchAll());
|
||||
this.results = Promise.all(promises);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async combineWithPriorities(): Promise<AvailableFlags.FlagList> {
|
||||
const list = await this.results;
|
||||
|
||||
return reduceFlagListArray(list);
|
||||
}
|
||||
|
||||
public getResult() {
|
||||
return this.results;
|
||||
}
|
||||
}
|
||||
|
||||
export const useFlagsResolver = (strategies: AvailableStrategies[]) => {
|
||||
const resolver = useRef<FlagsResolver>(new FlagsResolver(strategies));
|
||||
|
||||
return resolver.current;
|
||||
};
|
37
src/featureFlags/FlagsResolver/reduceFlagListArray.ts
Normal file
37
src/featureFlags/FlagsResolver/reduceFlagListArray.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import * as AvailableFlags from "./../availableFlags";
|
||||
import { FlagValue } from "./../FlagContent";
|
||||
|
||||
const byNotEmpty = (p: AvailableFlags.GeneralFlagList) =>
|
||||
Object.keys(p).length > 0;
|
||||
|
||||
const toFlagEntries = (
|
||||
p: Array<[string, FlagValue]>,
|
||||
c: AvailableFlags.GeneralFlagList,
|
||||
): Array<[string, FlagValue]> => [...p, ...Object.entries(c)];
|
||||
|
||||
const toWithoutPrefixes = ([name, value]: [string, FlagValue]): [
|
||||
string,
|
||||
FlagValue,
|
||||
] => [name.replace("FF_", ""), value];
|
||||
|
||||
const toSingleObject = (
|
||||
p: AvailableFlags.GeneralFlagList,
|
||||
[name, value]: [string, FlagValue],
|
||||
) => {
|
||||
if (!AvailableFlags.isSupported(name)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
if (!p[name]) {
|
||||
p[name] = value;
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
export const reduceFlagListArray = (flagListArray: AvailableFlags.FlagList[]) =>
|
||||
flagListArray
|
||||
.filter(byNotEmpty)
|
||||
.reduce(toFlagEntries, [])
|
||||
.map(toWithoutPrefixes)
|
||||
.reduce(toSingleObject, {} as AvailableFlags.FlagList);
|
9
src/featureFlags/Strategy.ts
Normal file
9
src/featureFlags/Strategy.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { FlagList } from "./availableFlags";
|
||||
|
||||
export interface Strategy {
|
||||
fetchAll(): Promise<FlagList>;
|
||||
}
|
||||
|
||||
export interface PersistableStrategy extends Strategy {
|
||||
store?(flags: FlagList): Promise<void>;
|
||||
}
|
66
src/featureFlags/availableFlags.ts
Normal file
66
src/featureFlags/availableFlags.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { FlagValue } from "./FlagContent";
|
||||
|
||||
interface FlagDefinition {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
content: {
|
||||
enabled: boolean;
|
||||
payload?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const AVAILABLE_FLAGS = [
|
||||
/*
|
||||
Before use any flag pleease an entry within this array,
|
||||
so the TS will infer the types, example:
|
||||
|
||||
{
|
||||
name: "flag1",
|
||||
displayName: "Flag 1",
|
||||
description: "some description",
|
||||
content: { enabled: false, value: "default" },
|
||||
} as const,
|
||||
*/
|
||||
|
||||
{
|
||||
name: "flag1",
|
||||
displayName: "Flag 1",
|
||||
description: "some description",
|
||||
content: { enabled: false, payload: "default" },
|
||||
} as const,
|
||||
{
|
||||
name: "flag2",
|
||||
displayName: "Flag 2",
|
||||
description: "some description 2",
|
||||
content: { enabled: false, payload: "default2" },
|
||||
} as const,
|
||||
] satisfies FlagDefinition[];
|
||||
|
||||
type TypedEntry = (typeof AVAILABLE_FLAGS)[number];
|
||||
type GeneralEntry = TypedEntry extends never ? FlagDefinition : TypedEntry;
|
||||
export type Entry = TypedEntry;
|
||||
export type Name = TypedEntry["name"];
|
||||
export type FlagList = Record<Name, FlagValue>;
|
||||
export type GeneralFlagList = TypedEntry extends never
|
||||
? Record<string, FlagValue>
|
||||
: FlagList;
|
||||
|
||||
const toFlagValue = (p: GeneralFlagList, c: GeneralEntry) => {
|
||||
p[c.name] = new FlagValue(c.content.enabled, c.content.payload);
|
||||
return p;
|
||||
};
|
||||
|
||||
export const isSupported = (name: string): name is Name =>
|
||||
AVAILABLE_FLAGS.some(
|
||||
(e: FlagDefinition) => e.name === name || `FF_${e.name}` === name,
|
||||
);
|
||||
|
||||
export const asFlagValue = () =>
|
||||
AVAILABLE_FLAGS.reduce(toFlagValue, {} as FlagList);
|
||||
|
||||
export const asFlagInfoArray = (list: GeneralFlagList) =>
|
||||
AVAILABLE_FLAGS.map((el: GeneralEntry) => ({
|
||||
...el,
|
||||
content: list[el.name],
|
||||
}));
|
17
src/featureFlags/context.ts
Normal file
17
src/featureFlags/context.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
import { FlagList } from "./availableFlags";
|
||||
|
||||
const FeatureFlagsContext = createContext<FlagList | undefined>(undefined);
|
||||
|
||||
export const Provider = FeatureFlagsContext.Provider;
|
||||
|
||||
export const useFeatureFlagContext = () => {
|
||||
const context = useContext(FeatureFlagsContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("FeatureFlagsContext must be uset within its provider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
5
src/featureFlags/index.tsx
Normal file
5
src/featureFlags/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { FeatureFlagsProvider } from "./FeatureFlagsProvider";
|
||||
export { EnvVarsStrategy, LocalStorageStrategy } from "./strategies";
|
||||
export { useFlag } from "./useFlag";
|
||||
export { useAllFlags } from "./useAllFlags";
|
||||
export type { FlagList } from "./availableFlags";
|
8
src/featureFlags/strategies/DefaultsStrategy.ts
Normal file
8
src/featureFlags/strategies/DefaultsStrategy.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import * as AvailableFlags from "../availableFlags";
|
||||
import { Strategy } from "../Strategy";
|
||||
|
||||
export class DefaultsStrategy implements Strategy {
|
||||
fetchAll(): Promise<AvailableFlags.FlagList> {
|
||||
return Promise.resolve(AvailableFlags.asFlagValue());
|
||||
}
|
||||
}
|
5
src/featureFlags/strategies/EnvVarsStrategy.ts
Normal file
5
src/featureFlags/strategies/EnvVarsStrategy.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { ObjectStorageStrategy } from "./ObjectStorageStrategy";
|
||||
|
||||
export class EnvVarsStrategy extends ObjectStorageStrategy {
|
||||
sourceObject = FLAGS;
|
||||
}
|
5
src/featureFlags/strategies/LocalStorageStrategy.ts
Normal file
5
src/featureFlags/strategies/LocalStorageStrategy.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { ObjectStorageStrategy } from "./ObjectStorageStrategy";
|
||||
|
||||
export class LocalStorageStrategy extends ObjectStorageStrategy {
|
||||
sourceObject = localStorage;
|
||||
}
|
26
src/featureFlags/strategies/ObjectStorageStrategy.ts
Normal file
26
src/featureFlags/strategies/ObjectStorageStrategy.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { FlagList, GeneralFlagList } from "../availableFlags";
|
||||
import { FlagValue } from "../FlagContent";
|
||||
import { Strategy } from "../Strategy";
|
||||
import * as AvailableFlags from "./../availableFlags";
|
||||
|
||||
const byFlagPrefix = ([key, _]: [string, string]) => key.startsWith("FF");
|
||||
|
||||
const toFlagList = (p: GeneralFlagList, [name, value]: [string, string]) => {
|
||||
if (AvailableFlags.isSupported(name)) {
|
||||
p[name] = FlagValue.fromString(value);
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
export abstract class ObjectStorageStrategy implements Strategy {
|
||||
sourceObject: Record<string, string> = {};
|
||||
|
||||
fetchAll(): Promise<FlagList> {
|
||||
const result = Object.entries(this.sourceObject)
|
||||
.filter(byFlagPrefix)
|
||||
.reduce(toFlagList, {} as FlagList);
|
||||
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
6
src/featureFlags/strategies/index.ts
Normal file
6
src/featureFlags/strategies/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { EnvVarsStrategy } from "./EnvVarsStrategy";
|
||||
import { LocalStorageStrategy } from "./LocalStorageStrategy";
|
||||
|
||||
export { EnvVarsStrategy, LocalStorageStrategy };
|
||||
|
||||
export type AvailableStrategies = EnvVarsStrategy | LocalStorageStrategy;
|
7
src/featureFlags/useAllFlags.ts
Normal file
7
src/featureFlags/useAllFlags.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { useFeatureFlagContext } from "./context";
|
||||
|
||||
export const useAllFlags = () => {
|
||||
const context = useFeatureFlagContext();
|
||||
|
||||
return context;
|
||||
};
|
14
src/featureFlags/useFlag.ts
Normal file
14
src/featureFlags/useFlag.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Name as FlagName } from "./availableFlags";
|
||||
import { useFeatureFlagContext } from "./context";
|
||||
import { FlagValue } from "./FlagContent";
|
||||
|
||||
export const useFlag = (name: FlagName) => {
|
||||
const context = useFeatureFlagContext();
|
||||
const flag = context[name];
|
||||
|
||||
if (!flag) {
|
||||
return FlagValue.empty();
|
||||
}
|
||||
|
||||
return context[name];
|
||||
};
|
8
src/featureFlags/useFlagsInfo.ts
Normal file
8
src/featureFlags/useFlagsInfo.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import * as AvailableFlags from "./availableFlags";
|
||||
import { useAllFlags } from "./useAllFlags";
|
||||
|
||||
export const useFlagsInfo = () => {
|
||||
const allFlags = useAllFlags();
|
||||
|
||||
return AvailableFlags.asFlagInfoArray(allFlags);
|
||||
};
|
1
src/hooks/useFlags/env/const.ts
vendored
1
src/hooks/useFlags/env/const.ts
vendored
|
@ -1 +0,0 @@
|
|||
export const ENV_FLAG_PREFIX = "FF_";
|
34
src/hooks/useFlags/env/helpers.ts
vendored
34
src/hooks/useFlags/env/helpers.ts
vendored
|
@ -1,34 +0,0 @@
|
|||
import camelCase from "lodash/camelCase";
|
||||
import snakeCase from "lodash/snakeCase";
|
||||
|
||||
import { ENV_FLAG_PREFIX } from "./const";
|
||||
|
||||
export const envNameToFlagName = (envName: string) => {
|
||||
const name = envName.split(ENV_FLAG_PREFIX)[1];
|
||||
return camelCase(name);
|
||||
};
|
||||
|
||||
export const flagNameToEnvName = (flagName: string) =>
|
||||
`${ENV_FLAG_PREFIX}${snakeCase(flagName).toUpperCase()}`;
|
||||
|
||||
/**
|
||||
Referencing an virtual constant FLAGS, prepared by Vite. It populates env-based feature flags into client-side, under the virtual property FLAGS,
|
||||
Please do not use FLAGS constant directly anywhere.
|
||||
*/
|
||||
export const readFlagFromEnv = (flagName: string): string | undefined => {
|
||||
if (FLAGS) {
|
||||
return FLAGS[flagName];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const readAllFlagsFromEnv = (): Record<string, string> => {
|
||||
if (FLAGS) {
|
||||
return FLAGS;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const isFlagEnabled = (flag: string) => flag !== "" && flag !== "false";
|
2
src/hooks/useFlags/env/index.ts
vendored
2
src/hooks/useFlags/env/index.ts
vendored
|
@ -1,2 +0,0 @@
|
|||
export { useEnvFlags } from "./useEnvFlags";
|
||||
export { useAllEnvFlags } from "./useAllEnvFlags";
|
62
src/hooks/useFlags/env/useAllEnvFlags.test.tsx
vendored
62
src/hooks/useFlags/env/useAllEnvFlags.test.tsx
vendored
|
@ -1,62 +0,0 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useAllEnvFlags } from "./useAllEnvFlags";
|
||||
|
||||
describe("useAllEnvFlags hook", () => {
|
||||
afterEach(() => {
|
||||
delete FLAGS.FF_FLAG_ONE;
|
||||
delete FLAGS.FF_FLAG_TWO;
|
||||
});
|
||||
|
||||
test("should return all environment flags", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "1";
|
||||
FLAGS.FF_FLAG_TWO = "2";
|
||||
|
||||
const { result } = renderHook(() => useAllEnvFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
name: "flagOne",
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
{
|
||||
name: "flagTwo",
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should return empty array when there is no flags", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useAllEnvFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return array with disabled flags", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "";
|
||||
FLAGS.FF_FLAG_TWO = "false";
|
||||
|
||||
const { result } = renderHook(() => useAllEnvFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
name: "flagOne",
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "flagTwo",
|
||||
enabled: false,
|
||||
value: "false",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
16
src/hooks/useFlags/env/useAllEnvFlags.ts
vendored
16
src/hooks/useFlags/env/useAllEnvFlags.ts
vendored
|
@ -1,16 +0,0 @@
|
|||
import { FlagWithName } from "../types";
|
||||
import {
|
||||
envNameToFlagName,
|
||||
isFlagEnabled,
|
||||
readAllFlagsFromEnv,
|
||||
} from "./helpers";
|
||||
|
||||
export const useAllEnvFlags = (): FlagWithName[] => {
|
||||
const flags = readAllFlagsFromEnv();
|
||||
|
||||
return Object.entries(flags).map(([flagKey, flagValue]) => ({
|
||||
name: envNameToFlagName(flagKey),
|
||||
enabled: isFlagEnabled(flagValue),
|
||||
value: flagValue,
|
||||
}));
|
||||
};
|
77
src/hooks/useFlags/env/useEnvFlags.test.tsx
vendored
77
src/hooks/useFlags/env/useEnvFlags.test.tsx
vendored
|
@ -1,77 +0,0 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useEnvFlags } from "./useEnvFlags";
|
||||
|
||||
describe("useEnvFlags hook", () => {
|
||||
afterEach(() => {
|
||||
delete FLAGS.FF_FLAG_ONE;
|
||||
delete FLAGS.FF_FLAG_TWO;
|
||||
});
|
||||
|
||||
test("should return results for given flags when exists in process.env", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "1";
|
||||
FLAGS.FF_FLAG_TWO = "2";
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags(["flagOne", "flag_two"]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return results for given flags even when flag does not exist", () => {
|
||||
// Arrange && Act
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags(["flagOne", "flag_two"]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return empty object when not flags provided", () => {
|
||||
// Arrange && Act
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags([]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
|
||||
test("should return array with disabled flags", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "";
|
||||
FLAGS.FF_FLAG_TWO = "false";
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags(["flagOne", "flag_two"]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: false,
|
||||
value: "false",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
23
src/hooks/useFlags/env/useEnvFlags.ts
vendored
23
src/hooks/useFlags/env/useEnvFlags.ts
vendored
|
@ -1,23 +0,0 @@
|
|||
import { FlagsResults } from "../types";
|
||||
import { flagNameToEnvName, isFlagEnabled, readFlagFromEnv } from "./helpers";
|
||||
|
||||
export const useEnvFlags = <T extends readonly string[]>(
|
||||
flags: readonly [...T],
|
||||
): FlagsResults<T> =>
|
||||
flags.reduce((acc, flag) => {
|
||||
const envFlag = readFlagFromEnv(flagNameToEnvName(flag));
|
||||
|
||||
if (envFlag) {
|
||||
acc[flag] = {
|
||||
enabled: isFlagEnabled(envFlag),
|
||||
value: envFlag,
|
||||
};
|
||||
} else {
|
||||
acc[flag] = {
|
||||
enabled: false,
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as FlagsResults<T>);
|
|
@ -1,8 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
interface FlagsServiceProviderProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export const FlagsServiceProvider = ({ children }: FlagsServiceProviderProps) =>
|
||||
children;
|
|
@ -1,3 +0,0 @@
|
|||
export { useServiceFlags } from "./useServiceFlags";
|
||||
export { useAllServiceFlags } from "./useAllServiceFlags";
|
||||
export { FlagsServiceProvider } from "./flagsServiceProvider";
|
|
@ -1,17 +0,0 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useAllServiceFlags } from "./useAllServiceFlags";
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useAllServiceFlags hook", () => {
|
||||
test("should return all flags from flag service", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useAllServiceFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
import { FlagWithName } from "../types";
|
||||
|
||||
export const useAllServiceFlags = (): FlagWithName[] => [];
|
|
@ -1,32 +0,0 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useServiceFlags } from "./useServiceFlags";
|
||||
|
||||
describe("useServiceFlags", () => {
|
||||
test("should return flags with values", () => {
|
||||
// Arrange && Ac
|
||||
const { result } = renderHook(() =>
|
||||
useServiceFlags(["flagOne", "flag_two"]),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return empty object when not flags provided", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useServiceFlags([]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import { FlagsResults } from "../types";
|
||||
|
||||
export const useServiceFlags = <T extends readonly string[]>(
|
||||
flags: readonly [...T],
|
||||
): FlagsResults<T> =>
|
||||
flags.reduce((acc, flag) => {
|
||||
acc[flag] = {
|
||||
enabled: false,
|
||||
value: "",
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {} as FlagsResults<T>);
|
|
@ -1,7 +0,0 @@
|
|||
import { useAllEnvFlags, useEnvFlags } from "./env";
|
||||
import { useAllServiceFlags, useServiceFlags } from "./flagsService";
|
||||
|
||||
export const useFlags = FLAGS_SERVICE_ENABLED ? useServiceFlags : useEnvFlags;
|
||||
export const useAllFlags = FLAGS_SERVICE_ENABLED
|
||||
? useAllServiceFlags
|
||||
: useAllEnvFlags;
|
|
@ -1,12 +0,0 @@
|
|||
export interface Flag {
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FlagWithName extends Flag {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type FlagsResults<T extends readonly string[]> = {
|
||||
[Key in T[number]]: Flag;
|
||||
};
|
|
@ -52,11 +52,15 @@ import CustomAppsSection from "./custom-apps";
|
|||
import { CustomAppSections } from "./custom-apps/urls";
|
||||
import { CustomerSection } from "./customers";
|
||||
import DiscountSection from "./discounts";
|
||||
import {
|
||||
EnvVarsStrategy,
|
||||
FeatureFlagsProvider,
|
||||
LocalStorageStrategy,
|
||||
} from "./featureFlags";
|
||||
import GiftCardSection from "./giftCards";
|
||||
import { giftCardsSectionUrlName } from "./giftCards/urls";
|
||||
import { apolloClient, saleorClient } from "./graphql/client";
|
||||
import HomePage from "./home";
|
||||
import { FlagsServiceProvider } from "./hooks/useFlags/flagsService";
|
||||
import { useLocationState } from "./hooks/useLocationState";
|
||||
import { commonMessages } from "./intl";
|
||||
import NavigationSection from "./navigation";
|
||||
|
@ -117,7 +121,12 @@ const App: React.FC = () => (
|
|||
<ServiceWorker />
|
||||
<BackgroundTasksProvider>
|
||||
<AppStateProvider>
|
||||
<FlagsServiceProvider>
|
||||
<FeatureFlagsProvider
|
||||
strategies={[
|
||||
new LocalStorageStrategy(),
|
||||
new EnvVarsStrategy(),
|
||||
]}
|
||||
>
|
||||
<AuthProvider>
|
||||
<ShopProvider>
|
||||
<AppChannelProvider>
|
||||
|
@ -131,7 +140,7 @@ const App: React.FC = () => (
|
|||
</AppChannelProvider>
|
||||
</ShopProvider>
|
||||
</AuthProvider>
|
||||
</FlagsServiceProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</AppStateProvider>
|
||||
</BackgroundTasksProvider>
|
||||
</MessageManagerProvider>
|
||||
|
|
|
@ -17,6 +17,14 @@ import ProductUpdatePage, { ProductUpdatePageProps } from "./ProductUpdatePage";
|
|||
|
||||
const product = productFixture(placeholderImage);
|
||||
|
||||
jest.mock("@dashboard/hooks/useNavigator", () => {
|
||||
const actualModule = jest.requireActual("@dashboard/hooks/useNavigator");
|
||||
return {
|
||||
__esModule: true,
|
||||
...actualModule,
|
||||
};
|
||||
});
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
const useNavigator = jest.spyOn(_useNavigator, "default");
|
||||
jest.mock("@dashboard/components/RichTextEditor/RichTextEditor");
|
||||
|
|
1
types.d.ts
vendored
1
types.d.ts
vendored
|
@ -6,7 +6,6 @@ declare module "*.svg" {
|
|||
}
|
||||
|
||||
declare const FLAGS_SERVICE_ENABLED: boolean;
|
||||
declare const FLAGSMITH_ID: string;
|
||||
declare const FLAGS: Record<string, string>;
|
||||
|
||||
declare interface Window {
|
||||
|
|
|
@ -45,7 +45,6 @@ export default defineConfig(({ command, mode }) => {
|
|||
DEMO_MODE,
|
||||
CUSTOM_VERSION,
|
||||
FLAGS_SERVICE_ENABLED,
|
||||
FLAGSMITH_ID,
|
||||
} = env;
|
||||
|
||||
const base = STATIC_URL ?? "/";
|
||||
|
@ -132,7 +131,6 @@ export default defineConfig(({ command, mode }) => {
|
|||
*/
|
||||
...(isDev ? { global: {} } : {}),
|
||||
FLAGS_SERVICE_ENABLED: FLAGS_SERVICE_ENABLED === "true",
|
||||
FLAGSMITH_ID: JSON.stringify(FLAGSMITH_ID),
|
||||
// Keep all feature flags from env in global variable
|
||||
FLAGS: JSON.stringify(featureFlagsEnvs),
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue