diff --git a/apps/condo/pages/_app.tsx b/apps/condo/pages/_app.tsx index 2f247a73f15..b4ef0c2ab53 100644 --- a/apps/condo/pages/_app.tsx +++ b/apps/condo/pages/_app.tsx @@ -11,7 +11,7 @@ import { useRouter } from 'next/router' import React, { useMemo } from 'react' import { useDeepCompareEffect } from '@open-condo/codegen/utils/useDeepCompareEffect' -import { FeatureFlagsProvider, useFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext' +import { FeatureFlagsProvider, useFeatureFlags, FeaturesReady, withFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext' import * as AllIcons from '@open-condo/icons' import { extractReqLocale } from '@open-condo/locales/extractReqLocale' import { withApollo, WithApolloProps } from '@open-condo/next/apollo' @@ -28,6 +28,7 @@ import { hasFeature } from '@condo/domains/common/components/containers/FeatureF import GlobalStyle from '@condo/domains/common/components/containers/GlobalStyle' import YandexMetrika from '@condo/domains/common/components/containers/YandexMetrika' import { LayoutContextProvider } from '@condo/domains/common/components/LayoutContext' +import { Loader } from '@condo/domains/common/components/Loader' import { MenuItem } from '@condo/domains/common/components/MenuItem' import PopupSmart from '@condo/domains/common/components/PopupSmart' import { PostMessageProvider } from '@condo/domains/common/components/PostMessageProvider' @@ -446,44 +447,44 @@ const MyApp = ({ Component, pageProps }) => { - - - - {shouldDisplayCookieAgreement && } - - - - - - - - - - - - } headerAction={HeaderAction}> - + + + {shouldDisplayCookieAgreement && } + + + + + + + + + + + + } headerAction={HeaderAction}> + + }> { isEndTrialSubscriptionReminderPopupVisible && ( ) } - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -535,7 +536,9 @@ export default ( ssr: true, GET_ORGANIZATION_TO_USER_LINK_BY_ID_QUERY: GET_ORGANIZATION_EMPLOYEE_BY_ID_QUERY, })( - MyApp + withFeatureFlags({ ssr: true })( + MyApp + ) ) ) ) diff --git a/packages/featureflags/FeatureFlagsContext.tsx b/packages/featureflags/FeatureFlagsContext.tsx index dee67373895..372b5aef552 100644 --- a/packages/featureflags/FeatureFlagsContext.tsx +++ b/packages/featureflags/FeatureFlagsContext.tsx @@ -1,20 +1,35 @@ -import { GrowthBook, GrowthBookProvider, useGrowthBook } from '@growthbook/growthbook-react' +import { GrowthBook, GrowthBookProvider, useGrowthBook, FeaturesReady } from '@growthbook/growthbook-react' import get from 'lodash/get' +import isEmpty from 'lodash/isEmpty' import isEqual from 'lodash/isEqual' +import { NextPage } from 'next' import getConfig from 'next/config' -import { createContext, useCallback, useContext, useEffect } from 'react' - +import { createContext, useCallback, useContext, useEffect, useRef } from 'react' + +import { + DEBUG_RERENDERS, + DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER, + getContextIndependentWrappedInitialProps, + preventInfinityLoop, +} from '@open-condo/next/_utils' import { useAuth } from '@open-condo/next/auth' import { useOrganization } from '@open-condo/next/organization' + +const { + publicRuntimeConfig: { + serverUrl, + }, +} = getConfig() + const growthbook = new GrowthBook() const FEATURES_RE_FETCH_INTERVAL = 10 * 1000 type UseFlagValueType = (name: string) => T | null interface IFeatureFlagsContext { - useFlag: (name: string) => boolean, - useFlagValue: UseFlagValueType, + useFlag: (name: string) => boolean + useFlagValue: UseFlagValueType updateContext: (context) => void } @@ -26,21 +41,16 @@ const FeatureFlagsContext = createContext({ const useFeatureFlags = (): IFeatureFlagsContext => useContext(FeatureFlagsContext) -const FeatureFlagsProviderWrapper = ({ children }) => { +const FeatureFlagsProviderWrapper = ({ children, initFeatures = null }) => { const growthbook = useGrowthBook() - const { user } = useAuth() - const { organization } = useOrganization() + const { user, isLoading: userIsLoading } = useAuth() + const { organization, isLoading: organizationIsLoading } = useOrganization() + const featuresRef = useRef(initFeatures) const isSupport = get(user, 'isSupport', false) const isAdmin = get(user, 'isAdmin', false) const userId = get(user, 'id', null) - const { - publicRuntimeConfig: { - serverUrl, - }, - } = getConfig() - const updateContext = useCallback((context) => { const previousContext = growthbook.getAttributes() @@ -51,17 +61,27 @@ const FeatureFlagsProviderWrapper = ({ children }) => { useEffect(() => { const fetchFeatures = () => { - if (serverUrl) { - fetch(`${serverUrl}/api/features`) - .then((res) => res.json()) - .then((features) => { - const prev = growthbook.getFeatures() - if (!isEqual(prev, features)) { - growthbook.setFeatures(features) - } - }) - .catch(e => console.error(e)) - } + if (!serverUrl) return + + const prev = growthbook.getFeatures() + let next = prev + fetch(`${serverUrl}/api/features`) + .then((res) => res.json()) + .then((features) => { + next = features + }) + .catch(e => { + if (!growthbook.ready && isEmpty(prev)) { + // NOTE: we need to update features so that growthbook is ready to work + next = prev + } + console.error(e) + }) + .finally(() => { + if (!growthbook.ready || !isEqual(prev, next)) { + featuresRef.current = next + } + }) } fetchFeatures() @@ -72,6 +92,12 @@ const FeatureFlagsProviderWrapper = ({ children }) => { } }, [growthbook, serverUrl]) + useEffect(() => { + if (!featuresRef.current || userIsLoading || organizationIsLoading) return + + growthbook.setPayload({ features: featuresRef.current }) + }, [featuresRef.current, userIsLoading, organizationIsLoading]) + useEffect(() => { updateContext({ isSupport: isSupport || isAdmin, organization: get(organization, 'id'), userId }) }, [updateContext, isAdmin, isSupport, organization, userId]) @@ -87,14 +113,79 @@ const FeatureFlagsProviderWrapper = ({ children }) => { ) } -const FeatureFlagsProvider: React.FC = ({ children }) => { +const FeatureFlagsProvider: React.FC<{ initFeatures? }> = ({ children, initFeatures = null }) => { return ( - + {children} ) } -export { useFeatureFlags, FeatureFlagsProvider } +// @ts-ignore +if (DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER) FeatureFlagsProvider.whyDidYouRender = true + +const initOnRestore = async (ctx) => { + let features = null + const isOnServerSide = typeof window === 'undefined' + + if (isOnServerSide) { + try { + const response = await fetch(`${serverUrl}/api/features`) + features = await response.json() + } catch (error) { + console.error('Error while running `withFeatureFlags`', error) + features = null + } + } + + return { features } +} + +type WithFeatureFlagsProps = { + ssr?: boolean +} +export type WithFeatureFlags = (props: WithFeatureFlagsProps) => (PageComponent: NextPage) => NextPage + +const withFeatureFlags: WithFeatureFlags = ({ ssr = false }) => PageComponent => { + const WithFeatureFlags = ({ features, ...pageProps }) => { + if (DEBUG_RERENDERS) console.log('WithFeatureFlags()', features) + + return ( + + + + ) + } + + if (DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER) WithFeatureFlags.whyDidYouRender = true + + // Set the correct displayName in development + if (process.env.NODE_ENV !== 'production') { + const displayName = PageComponent.displayName || PageComponent.name || 'Component' + WithFeatureFlags.displayName = `withFeatureFlags(${displayName})` + } + + if (ssr || PageComponent.getInitialProps) { + WithFeatureFlags.getInitialProps = async (ctx) => { + if (DEBUG_RERENDERS) console.log('WithIntl.getInitialProps()', ctx) + const isOnServerSide = typeof window === 'undefined' + const { features } = await initOnRestore(ctx) + const pageProps = await getContextIndependentWrappedInitialProps(PageComponent, ctx) + + if (isOnServerSide) { + preventInfinityLoop(ctx) + } + + return { + ...pageProps, + features, + } + } + } + + return WithFeatureFlags +} + +export { useFeatureFlags, FeatureFlagsProvider, FeaturesReady, withFeatureFlags } diff --git a/packages/featureflags/featureToggleManager.js b/packages/featureflags/featureToggleManager.js index 09b2f91e543..9043cdc5995 100644 --- a/packages/featureflags/featureToggleManager.js +++ b/packages/featureflags/featureToggleManager.js @@ -1,8 +1,8 @@ const { GrowthBook } = require('@growthbook/growthbook') const { get } = require('lodash') -const fetch = require('node-fetch') const conf = require('@open-condo/config') +const { fetch } = require('@open-condo/keystone/fetch') const { getLogger } = require('@open-condo/keystone/logging') const { getRedisClient } = require('@open-condo/keystone/redis') const { getFeatureFlag } = require('@open-condo/keystone/test.utils') diff --git a/packages/featureflags/package.json b/packages/featureflags/package.json index 407ee5b51ba..8f441580f09 100644 --- a/packages/featureflags/package.json +++ b/packages/featureflags/package.json @@ -2,12 +2,12 @@ "name": "@open-condo/featureflags", "version": "1.0.0", "dependencies": { - "@growthbook/growthbook": "^0.18.1", - "@growthbook/growthbook-react": "^0.9.1", + "@growthbook/growthbook": "^1.0.1", + "@growthbook/growthbook-react": "^1.0.1", "@open-condo/config": "workspace:^", "@open-condo/keystone": "workspace:^", - "express": "^4.17.1", - "node-fetch": "^2.6.7" + "@open-condo/next": "workspace:^", + "express": "^4.17.1" }, "peerDependencies": { "next": ">=9.5.5", diff --git a/yarn.lock b/yarn.lock index c917218ffe8..c6d36241df8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10288,21 +10288,23 @@ __metadata: languageName: node linkType: hard -"@growthbook/growthbook-react@npm:^0.9.1": - version: 0.9.1 - resolution: "@growthbook/growthbook-react@npm:0.9.1" +"@growthbook/growthbook-react@npm:^1.0.1": + version: 1.0.1 + resolution: "@growthbook/growthbook-react@npm:1.0.1" dependencies: - "@growthbook/growthbook": ^0.18.1 + "@growthbook/growthbook": ^1.0.1 peerDependencies: react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 - checksum: 587b0536d9f0f9180798ff7221a4fed61696e77a3d3e5a782afd46cf0ac01f5519145985fe7bf62ab602eea9f899c1165af885ddabb0f7a8a3e48ac3d4abd6b3 + checksum: d13182390f1561e0f464f25205d08ab4e872a29c070bf5541fdab0383e294dd1f62996fa414356cdf0317e4a72415817b203d805b7ea1ef4c2ba7d880ff0edba languageName: node linkType: hard -"@growthbook/growthbook@npm:^0.18.1": - version: 0.18.1 - resolution: "@growthbook/growthbook@npm:0.18.1" - checksum: 202347550562487b47044f406415f26976ec1fabd0504658f84c6520d834ec8addf57be1ba5e73e4715d02da4c11f41649edbb6ad49b5d2b24ec46a497c5617f +"@growthbook/growthbook@npm:^1.0.1": + version: 1.0.1 + resolution: "@growthbook/growthbook@npm:1.0.1" + dependencies: + dom-mutator: ^0.6.0 + checksum: 385a6c0a480e1a217cc8000147637eacde1ec04c20b424fb6ac59b1102b28ee5be89e86b2bff4227cfc46993b1870a654cd1216fc4833954135be14f8e6fd1a4 languageName: node linkType: hard @@ -12410,12 +12412,12 @@ __metadata: version: 0.0.0-use.local resolution: "@open-condo/featureflags@workspace:packages/featureflags" dependencies: - "@growthbook/growthbook": ^0.18.1 - "@growthbook/growthbook-react": ^0.9.1 + "@growthbook/growthbook": ^1.0.1 + "@growthbook/growthbook-react": ^1.0.1 "@open-condo/config": "workspace:^" "@open-condo/keystone": "workspace:^" + "@open-condo/next": "workspace:^" express: ^4.17.1 - node-fetch: ^2.6.7 peerDependencies: next: ">=9.5.5" react: ">=16.13.1" @@ -25253,6 +25255,13 @@ __metadata: languageName: node linkType: hard +"dom-mutator@npm:^0.6.0": + version: 0.6.0 + resolution: "dom-mutator@npm:0.6.0" + checksum: f6b32500d9d71f379940022057434db38fd404036968fdbf0d547b31a819c0ad2160edbf67f72e00b8ec7787b5ca04f19d5082fbf33f3644642b16f21e04cbf1 + languageName: node + linkType: hard + "dom-serializer@npm:0, dom-serializer@npm:^0.2.1": version: 0.2.2 resolution: "dom-serializer@npm:0.2.2"