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"