mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
pro-console: integrate onboarding wizard
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5559 Co-authored-by: Rishichandra Wawhal <27274869+wawhal@users.noreply.github.com> GitOrigin-RevId: 35da0aa3c22f030f78d90bdd0ef6a7526c566016
This commit is contained in:
parent
37601d7303
commit
150fbaea7c
@ -11,6 +11,8 @@ export {
|
|||||||
export { fetchConsoleNotifications } from '../src/components/Main/Actions';
|
export { fetchConsoleNotifications } from '../src/components/Main/Actions';
|
||||||
export { default as NotificationSection } from '../src/components/Main/NotificationSection';
|
export { default as NotificationSection } from '../src/components/Main/NotificationSection';
|
||||||
export { default as Onboarding } from '../src/components/Common/Onboarding';
|
export { default as Onboarding } from '../src/components/Common/Onboarding';
|
||||||
|
export { OnboardingWizard } from '../src/features/OnboardingWizard';
|
||||||
|
export { makeGrowthExperimentsClient } from '../src/features/GrowthExperiments'
|
||||||
export { default as PageNotFound } from '../src/components/Error/PageNotFound';
|
export { default as PageNotFound } from '../src/components/Error/PageNotFound';
|
||||||
export * from '../src/new-components/Button/';
|
export * from '../src/new-components/Button/';
|
||||||
export * from '../src/new-components/Tooltip/';
|
export * from '../src/new-components/Tooltip/';
|
||||||
|
@ -123,6 +123,7 @@ export type EnvVars = {
|
|||||||
consoleType?: ConsoleType;
|
consoleType?: ConsoleType;
|
||||||
eeMode?: string;
|
eeMode?: string;
|
||||||
consoleId?: string;
|
consoleId?: string;
|
||||||
|
userRole?: string;
|
||||||
} & (
|
} & (
|
||||||
| OSSServerEnv
|
| OSSServerEnv
|
||||||
| CloudServerEnv
|
| CloudServerEnv
|
||||||
@ -173,7 +174,7 @@ const globals = {
|
|||||||
hasuraCloudProjectId: window.__env?.projectID,
|
hasuraCloudProjectId: window.__env?.projectID,
|
||||||
cloudDataApiUrl: `${window.location?.protocol}//data.${window.__env?.cloudRootDomain}`,
|
cloudDataApiUrl: `${window.location?.protocol}//data.${window.__env?.cloudRootDomain}`,
|
||||||
luxDataHost: window.__env?.luxDataHost,
|
luxDataHost: window.__env?.luxDataHost,
|
||||||
userRole: undefined, // userRole is not applicable for the OSS console
|
userRole: window.__env?.userRole || undefined,
|
||||||
consoleType: window.__env?.consoleType || '',
|
consoleType: window.__env?.consoleType || '',
|
||||||
eeMode: window.__env?.eeMode === 'true',
|
eeMode: window.__env?.eeMode === 'true',
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import Endpoints from '@/Endpoints';
|
||||||
|
import { Api } from '@/hooks/apiUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls hasura cloud data service with provided query and variables. Uses the common `fetch` api client.
|
||||||
|
* Returns a promise which either resolves the data or throws an error. Optionally pass a transform function
|
||||||
|
* to transform the response data. This can be transformed into a hook, and directly use headers from
|
||||||
|
* `useAppSelector` hook if required. This can also be passed to react query as the `queryFn` if required.
|
||||||
|
*/
|
||||||
|
export function cloudDataServiceApiClient<
|
||||||
|
ResponseData,
|
||||||
|
TransformedData = ResponseData
|
||||||
|
>(
|
||||||
|
query: string,
|
||||||
|
variables: Record<string, string>,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
transformFn?: (data: ResponseData) => TransformedData
|
||||||
|
): Promise<TransformedData> {
|
||||||
|
return Api.post<ResponseData, TransformedData>(
|
||||||
|
{
|
||||||
|
url: Endpoints.luxDataGraphql,
|
||||||
|
headers,
|
||||||
|
body: {
|
||||||
|
query,
|
||||||
|
variables: variables || {},
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
transformFn
|
||||||
|
);
|
||||||
|
}
|
20
console/src/features/GrowthExperiments/constants.ts
Normal file
20
console/src/features/GrowthExperiments/constants.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* GraphQl query to fetch all growth experiments data related to the current user.
|
||||||
|
*/
|
||||||
|
export const query = `
|
||||||
|
query {
|
||||||
|
experiments_config {
|
||||||
|
experiment
|
||||||
|
metadata
|
||||||
|
status
|
||||||
|
}
|
||||||
|
experiments_cohort {
|
||||||
|
experiment
|
||||||
|
activity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const growthExperimentsIds = {
|
||||||
|
onboardingWizardV1: 'console_onboarding_wizard_v1',
|
||||||
|
};
|
@ -0,0 +1,66 @@
|
|||||||
|
import globals from '@/Globals';
|
||||||
|
import { cloudDataServiceApiClient } from './cloudDataServiceApiClient';
|
||||||
|
import { ExperimentConfig, ExperimentsResponseData } from './types';
|
||||||
|
import { query } from './constants';
|
||||||
|
import { transformFn } from './utils';
|
||||||
|
|
||||||
|
export type GrowthExperimentsClient = {
|
||||||
|
setAllExperimentConfig: () => Promise<void>;
|
||||||
|
getAllExperimentConfig: () => ExperimentConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all growth experiments data for the given user. Not making it a hook as this could be required in legacy class components.
|
||||||
|
* Hence `setAllExperimentConfig` needs to be passed the correct headers from calling component. If caller is hook, use `useAppSelector`
|
||||||
|
* to get the headers. Class based caller needs to subscribe to the global store to get headers.
|
||||||
|
*/
|
||||||
|
export function makeGrowthExperimentsClient(): GrowthExperimentsClient {
|
||||||
|
/**
|
||||||
|
* Experiments config data, transformed in required format.
|
||||||
|
*/
|
||||||
|
let allExperimentsConfig: ExperimentConfig[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes variable `allExperimentsConfig` consistent with the server state. Call this in top level of your app to set
|
||||||
|
* `allExperimentsConfig`. Also, when a server state mutation is done, use this function to keep data in sync.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const setAllExperimentConfig = () => {
|
||||||
|
/*
|
||||||
|
* Gracefully exit if current context is not cloud-console
|
||||||
|
* and current user is not project owner
|
||||||
|
*/
|
||||||
|
if (globals.consoleType !== 'cloud' || globals.userRole !== 'owner') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloud uses cookie-based auth, so does not require an admin secret
|
||||||
|
const headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cloudDataServiceApiClient<
|
||||||
|
ExperimentsResponseData,
|
||||||
|
ExperimentConfig[]
|
||||||
|
>(query, {}, headers, transformFn)
|
||||||
|
.then(res => {
|
||||||
|
allExperimentsConfig = res;
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Possible enhancement: Add a retry mechanism
|
||||||
|
console.error(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns latest `allExperimentsConfig` data. Note that it doesn't subscribe the calling component to changes in `allExperimentsConfig`.
|
||||||
|
* Update the component UI manually if you're also doing a server state mutation using `setAllExperimentConfig`.
|
||||||
|
*/
|
||||||
|
const getAllExperimentConfig = () => {
|
||||||
|
return allExperimentsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getAllExperimentConfig, setAllExperimentConfig };
|
||||||
|
}
|
7
console/src/features/GrowthExperiments/index.ts
Normal file
7
console/src/features/GrowthExperiments/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { cloudDataServiceApiClient } from './cloudDataServiceApiClient';
|
||||||
|
export {
|
||||||
|
makeGrowthExperimentsClient,
|
||||||
|
GrowthExperimentsClient,
|
||||||
|
} from './growthExperimentsConfigClient';
|
||||||
|
export { growthExperimentsIds } from './constants';
|
||||||
|
export { ExperimentConfig } from './types';
|
24
console/src/features/GrowthExperiments/types.ts
Normal file
24
console/src/features/GrowthExperiments/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export type ExperimentConfig = {
|
||||||
|
experiment: string;
|
||||||
|
status: string;
|
||||||
|
metadata: ExperimentsResponseData['data']['experiments_config'][0]['metadata'];
|
||||||
|
userActivity: ExperimentsResponseData['data']['experiments_cohort'][0]['activity'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExperimentsResponseData = {
|
||||||
|
data: {
|
||||||
|
experiments_config: [
|
||||||
|
{
|
||||||
|
experiment: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
experiments_cohort: [
|
||||||
|
{
|
||||||
|
experiment: string;
|
||||||
|
activity: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
19
console/src/features/GrowthExperiments/utils.ts
Normal file
19
console/src/features/GrowthExperiments/utils.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ExperimentConfig, ExperimentsResponseData } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms server returned data to the required format.
|
||||||
|
*/
|
||||||
|
export function transformFn(data: ExperimentsResponseData): ExperimentConfig[] {
|
||||||
|
return data.data.experiments_config.map(experimentConfig => {
|
||||||
|
const experimentCohort = data.data.experiments_cohort.find(
|
||||||
|
cohort => cohort.experiment === experimentConfig.experiment
|
||||||
|
);
|
||||||
|
const userActivity = experimentCohort?.activity ?? {};
|
||||||
|
return {
|
||||||
|
experiment: experimentConfig.experiment,
|
||||||
|
status: experimentConfig.status,
|
||||||
|
metadata: experimentConfig.metadata,
|
||||||
|
userActivity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@ -7,4 +7,18 @@ export default {
|
|||||||
component: Root,
|
component: Root,
|
||||||
} as ComponentMeta<typeof Root>;
|
} as ComponentMeta<typeof Root>;
|
||||||
|
|
||||||
export const Base: Story = () => <Root />;
|
const mockGrowthClient = {
|
||||||
|
getAllExperimentConfig: () => [
|
||||||
|
{
|
||||||
|
experiment: 'console_onboarding_wizard_v1',
|
||||||
|
status: 'enabled',
|
||||||
|
metadata: {},
|
||||||
|
userActivity: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setAllExperimentConfig: () => Promise.resolve(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Base: Story = () => (
|
||||||
|
<Root growthExperimentsClient={mockGrowthClient} />
|
||||||
|
);
|
||||||
|
@ -1,19 +1,35 @@
|
|||||||
import React, { useReducer } from 'react';
|
import React from 'react';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { TopHeaderBar, ConnectDBScreen } from './components';
|
import { TopHeaderBar, ConnectDBScreen } from './components';
|
||||||
|
import { useWizardState } from './hooks';
|
||||||
|
import { GrowthExperimentsClient } from '../GrowthExperiments';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
growthExperimentsClient: GrowthExperimentsClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent container for the onboarding wizard. Takes care of assembling and rendering all steps.
|
||||||
|
*/
|
||||||
|
export function Root(props: Props) {
|
||||||
|
const { growthExperimentsClient } = props;
|
||||||
|
|
||||||
export function Root() {
|
|
||||||
// dialog cannot be reopened once closed
|
// dialog cannot be reopened once closed
|
||||||
const [isWizardOpen, closeWizard] = useReducer(() => false, true);
|
const { isWizardOpen, skipOnboarding, completeOnboarding, setIsWizardOpen } =
|
||||||
|
useWizardState(growthExperimentsClient);
|
||||||
|
|
||||||
// this dialog is being used to create a layover component over the whole app using react portal, and other handy functionalities radix dialog provides, which otherwise we'll have to implement manually
|
// Radix dialog is being used for creating a layover component over the whole app using react portal,
|
||||||
// note that we have a common radix dialog, but that component has very specific styling, doesn't include react portal, and it did not make sense to extend that to fit this particular one-off use case
|
// and for other handy functionalities radix-dialog provides, which otherwise we'll have to implement manually.
|
||||||
|
// Not extending the common dialog, as it does not make sense to update common component to fit this one-off use case.
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={isWizardOpen} onOpenChange={closeWizard}>
|
<Dialog.Root open={isWizardOpen} onOpenChange={setIsWizardOpen}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Content className="fixed top-0 w-full h-full focus:outline-none bg-gray-50 overflow-hidden z-[101]">
|
<Dialog.Content className="fixed top-0 w-full h-full focus:outline-none bg-gray-50 overflow-hidden z-[101]">
|
||||||
<TopHeaderBar />
|
<TopHeaderBar />
|
||||||
<ConnectDBScreen closeWizard={closeWizard} />
|
<ConnectDBScreen
|
||||||
|
skipOnboarding={skipOnboarding}
|
||||||
|
completeOnboarding={completeOnboarding}
|
||||||
|
/>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
@ -9,7 +9,9 @@ export default {
|
|||||||
component: ConnectDBScreen,
|
component: ConnectDBScreen,
|
||||||
} as ComponentMeta<typeof ConnectDBScreen>;
|
} as ComponentMeta<typeof ConnectDBScreen>;
|
||||||
|
|
||||||
export const Base: Story = () => <ConnectDBScreen closeWizard={() => {}} />;
|
export const Base: Story = () => (
|
||||||
|
<ConnectDBScreen skipOnboarding={() => {}} completeOnboarding={() => {}} />
|
||||||
|
);
|
||||||
|
|
||||||
Base.play = async ({ canvasElement }) => {
|
Base.play = async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
@ -5,19 +5,19 @@ import _push from '../../../../components/Services/Data/push';
|
|||||||
import { OnboardingAnimation, OnboardingAnimationNavbar } from './components';
|
import { OnboardingAnimation, OnboardingAnimationNavbar } from './components';
|
||||||
|
|
||||||
type ConnectDBScreenProps = {
|
type ConnectDBScreenProps = {
|
||||||
closeWizard: () => void;
|
skipOnboarding: () => void;
|
||||||
|
completeOnboarding: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ConnectDBScreen(props: ConnectDBScreenProps) {
|
export function ConnectDBScreen(props: ConnectDBScreenProps) {
|
||||||
const { closeWizard } = props;
|
const { skipOnboarding, completeOnboarding } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
// we should change the route to `/data` first, and then close the wizard.
|
// TODO: Due to routing being slow on prod, but wizard closing instantaneously, this causes
|
||||||
// this is because routing is slow on prod, but wizard closes instantaneously.
|
// a flicker of `<Api />` tab before routing to `/data`.
|
||||||
// if we close wizard first, it will show a flicker of `<Api />` tab before routing to `/data`.
|
|
||||||
dispatch(_push(`/data/manage/connect`));
|
dispatch(_push(`/data/manage/connect`));
|
||||||
closeWizard();
|
completeOnboarding();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,7 +34,7 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="cursor-pointer text-secondary text-sm hover:text-secondary-dark">
|
<div className="cursor-pointer text-secondary text-sm hover:text-secondary-dark">
|
||||||
<div data-trackid="onboarding-skip-button" onClick={closeWizard}>
|
<div data-trackid="onboarding-skip-button" onClick={skipOnboarding}>
|
||||||
Skip setup, continue to dashboard
|
Skip setup, continue to dashboard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
29
console/src/features/OnboardingWizard/hooks/constants.ts
Normal file
29
console/src/features/OnboardingWizard/hooks/constants.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { growthExperimentsIds } from '@/features/GrowthExperiments';
|
||||||
|
import globals from '@/Globals';
|
||||||
|
|
||||||
|
export const experimentId = growthExperimentsIds.onboardingWizardV1;
|
||||||
|
|
||||||
|
export const graphQlMutation = `
|
||||||
|
mutation ($projectId: uuid!, $experimentId: String!, $kind: String!) {
|
||||||
|
trackExperimentsCohortActivity(experiment: $experimentId, payload: {kind: $kind, project_id: $projectId}) {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const projectId = globals.hasuraCloudProjectId;
|
||||||
|
|
||||||
|
const mutationVariables = {
|
||||||
|
...(projectId && { projectId }),
|
||||||
|
experimentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onboardingCompleteVariables = {
|
||||||
|
...mutationVariables,
|
||||||
|
kind: 'onboarding_complete',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const skippedOnboardingVariables = {
|
||||||
|
...mutationVariables,
|
||||||
|
kind: 'skipped_onboarding',
|
||||||
|
};
|
1
console/src/features/OnboardingWizard/hooks/index.ts
Normal file
1
console/src/features/OnboardingWizard/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useWizardState } from './useWizardState';
|
@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
cloudDataServiceApiClient,
|
||||||
|
GrowthExperimentsClient,
|
||||||
|
} from '@/features/GrowthExperiments';
|
||||||
|
import {
|
||||||
|
experimentId,
|
||||||
|
graphQlMutation,
|
||||||
|
onboardingCompleteVariables,
|
||||||
|
skippedOnboardingVariables,
|
||||||
|
} from './constants';
|
||||||
|
import { isExperimentActive, shouldShowOnboarding } from './utils';
|
||||||
|
|
||||||
|
type ResponseDataOnMutation = {
|
||||||
|
data: {
|
||||||
|
trackExperimentsCohortActivity: {
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWizardState(
|
||||||
|
growthExperimentsClient: GrowthExperimentsClient
|
||||||
|
) {
|
||||||
|
// lux works with cookie auth and doesn't require admin-secret header
|
||||||
|
const headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getAllExperimentConfig, setAllExperimentConfig } =
|
||||||
|
growthExperimentsClient;
|
||||||
|
const experimentData = getAllExperimentConfig();
|
||||||
|
|
||||||
|
const [isWizardOpen, setIsWizardOpen] = useState(
|
||||||
|
shouldShowOnboarding(experimentData, experimentId) &&
|
||||||
|
isExperimentActive(experimentData, experimentId)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsWizardOpen(
|
||||||
|
shouldShowOnboarding(experimentData, experimentId) &&
|
||||||
|
isExperimentActive(experimentData, experimentId)
|
||||||
|
);
|
||||||
|
}, [experimentData]);
|
||||||
|
|
||||||
|
const skipOnboarding = () => {
|
||||||
|
setIsWizardOpen(false);
|
||||||
|
|
||||||
|
// mutate server data
|
||||||
|
cloudDataServiceApiClient<ResponseDataOnMutation, ResponseDataOnMutation>(
|
||||||
|
graphQlMutation,
|
||||||
|
skippedOnboardingVariables,
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// refetch the fresh data and update the `growthExperimentConfigClient`
|
||||||
|
setAllExperimentConfig();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeOnboarding = () => {
|
||||||
|
setIsWizardOpen(false);
|
||||||
|
|
||||||
|
// mutate server data
|
||||||
|
cloudDataServiceApiClient<ResponseDataOnMutation, ResponseDataOnMutation>(
|
||||||
|
graphQlMutation,
|
||||||
|
onboardingCompleteVariables,
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// refetch the fresh data and update the `growthExperimentConfigClient`
|
||||||
|
setAllExperimentConfig();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { isWizardOpen, skipOnboarding, completeOnboarding, setIsWizardOpen };
|
||||||
|
}
|
27
console/src/features/OnboardingWizard/hooks/utils.ts
Normal file
27
console/src/features/OnboardingWizard/hooks/utils.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ExperimentConfig } from '@/features/GrowthExperiments';
|
||||||
|
|
||||||
|
export function isExperimentActive(
|
||||||
|
experimentsData: ExperimentConfig[],
|
||||||
|
experimentId: string
|
||||||
|
) {
|
||||||
|
const experimentData = experimentsData?.find(
|
||||||
|
experimentConfig => experimentConfig.experiment === experimentId
|
||||||
|
);
|
||||||
|
return experimentData && experimentData?.status === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowOnboarding(
|
||||||
|
experimentsData: ExperimentConfig[],
|
||||||
|
experimentId: string
|
||||||
|
) {
|
||||||
|
const experimentData = experimentsData?.find(
|
||||||
|
experimentConfig => experimentConfig.experiment === experimentId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
experimentData?.userActivity?.onboarding_complete ||
|
||||||
|
experimentData?.userActivity?.skipped_onboarding
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
@ -9,6 +9,7 @@ interface IApiArgs {
|
|||||||
url: string;
|
url: string;
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
body?: Record<any, any>;
|
body?: Record<any, any>;
|
||||||
|
credentials?: 'include' | 'omit' | 'same-origin';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchApi<T = unknown, V = T>(
|
async function fetchApi<T = unknown, V = T>(
|
||||||
@ -16,11 +17,12 @@ async function fetchApi<T = unknown, V = T>(
|
|||||||
dataTransform?: (data: T) => V
|
dataTransform?: (data: T) => V
|
||||||
): Promise<V> {
|
): Promise<V> {
|
||||||
try {
|
try {
|
||||||
const { headers, url, method, body } = args;
|
const { headers, url, method, body, credentials } = args;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers,
|
headers,
|
||||||
method,
|
method,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
credentials,
|
||||||
});
|
});
|
||||||
const contentType = response.headers.get('Content-Type');
|
const contentType = response.headers.get('Content-Type');
|
||||||
const isResponseJson = `${contentType}`.includes('application/json');
|
const isResponseJson = `${contentType}`.includes('application/json');
|
||||||
|
Loading…
Reference in New Issue
Block a user