From a03cc58e5bebcbcb75ac0149a91a71fc4024359e Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 21 Sep 2022 12:47:40 +0200 Subject: [PATCH] feat: remove app next.js built-time environment variable requirement (#382) * feat: retreive stripe public key via runtime environment variable instead of a build-time environment variable * feat: retreive google and github enabled environment variable via runtime environment variable instead of a build-time environment variable * feat: retreive app base url environment variable via runtime environment variable instead of a build-time environment variable * feat: retreive mixpanel token environment variable via runtime environment variable instead of a build-time environment variable * lazy initialize app info * feat: provide ga tarcking id and crisp website id via environment variable * make docs url optional * feat: load sentry config from environment variables * document hive app environment variables * add application dockerfile * add docker build instructions * lul * set working directory * pls fix * lol this is apollo-router not graphql-hive * feat: only show sentry stuff if sentry environment variables are set * use LTS node version * No mixpanel * Fallback Co-authored-by: Kamil Kisiela --- .github/workflows/cd.yaml | 2 +- .github/workflows/ci.yaml | 62 ++++++++++++++++-- deployment/services/app.ts | 34 +++++++--- packages/web/app/.env.template | 7 +-- packages/web/app/Dockerfile | 24 +++++++ packages/web/app/README.md | 63 +++++++++++++++++++ packages/web/app/modules.d.ts | 19 +++++- .../[projectId]/[targetId]/operations.tsx | 3 +- .../app/pages/[orgId]/[projectId]/index.tsx | 3 +- packages/web/app/pages/[orgId]/index.tsx | 6 +- .../app/pages/[orgId]/subscription/index.tsx | 19 +++++- packages/web/app/pages/_document.tsx | 46 +++++++++++++- .../web/app/pages/api/auth/[[...path]].ts | 2 +- packages/web/app/pages/api/github/callback.ts | 2 +- .../app/pages/api/github/connect/[orgId].ts | 2 +- .../app/pages/api/github/setup-callback.ts | 2 +- packages/web/app/pages/api/slack/callback.ts | 2 +- .../app/pages/api/slack/connect/[orgId].ts | 2 +- packages/web/app/sentry.config.ts | 14 ++--- .../app/src/components/common/Navigation.tsx | 15 +++-- .../app/src/components/get-started/wizard.tsx | 29 +++------ .../src/components/layouts/organization.tsx | 3 +- .../web/app/src/components/v2/empty-list.tsx | 13 ++-- packages/web/app/src/components/v2/header.tsx | 16 +++-- .../src/components/v2/modals/connect-lab.tsx | 17 ++--- .../components/v2/modals/connect-schema.tsx | 3 +- packages/web/app/src/config/app-info.ts | 25 +++++--- packages/web/app/src/config/backend-config.ts | 14 ++--- .../web/app/src/config/frontend-config.ts | 6 +- packages/web/app/src/constants.ts | 5 +- .../app/src/lib/billing/stripe-public-key.ts | 9 +++ packages/web/app/src/lib/billing/stripe.tsx | 21 ++++--- packages/web/app/src/lib/docs-url.ts | 7 +++ turbo.json | 8 +-- 34 files changed, 380 insertions(+), 125 deletions(-) create mode 100644 packages/web/app/Dockerfile create mode 100644 packages/web/app/README.md create mode 100644 packages/web/app/src/lib/billing/stripe-public-key.ts create mode 100644 packages/web/app/src/lib/docs-url.ts diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 9c9c5084b..a295436c5 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -111,7 +111,7 @@ jobs: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME: ${{ github.repository }}/apollo-router permissions: contents: read diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7362c9d6f..c35574e17 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -109,8 +109,6 @@ jobs: - name: Build run: yarn workspace integration-tests run build-and-pack - env: - NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }} - name: Pull images run: docker-compose -f integration-tests/docker-compose.yml pull @@ -119,6 +117,7 @@ jobs: run: yarn workspace integration-tests run dockest timeout-minutes: 15 env: + STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }} STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} SUPERTOKENS_CONNECTION_URI: http://127.0.0.1:3567 SUPERTOKENS_API_KEY: bubatzbieber6942096420 @@ -177,8 +176,6 @@ jobs: - name: Build if: steps.feature_flags.outputs.detected == 'true' run: yarn workspace integration-tests run build-and-pack - env: - NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }} - name: Pull images if: steps.feature_flags.outputs.detected == 'true' @@ -189,6 +186,7 @@ jobs: run: yarn workspace integration-tests run dockest timeout-minutes: 15 env: + STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }} STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} SUPERTOKENS_CONNECTION_URI: http://127.0.0.1:3567 SUPERTOKENS_API_KEY: bubatzbieber6942096420 @@ -404,3 +402,59 @@ jobs: - name: Lint Prettier run: yarn lint:prettier + + publish_app: + needs: setup + name: Publish for Docker + runs-on: ubuntu-latest + timeout-minutes: 40 + + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Node + uses: actions/setup-node@v2 + with: + node-version-file: .node-version + + - name: Pull node_modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ github.sha }} + + - name: Setup Turbo + run: node ./scripts/turborepo-setup.js + + - name: Generate Types + run: yarn graphql:generate + + - name: Build + run: yarn build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + file: packages/web/app/Dockerfile + context: ./packages/web/app + push: true + tags: ${{ env.REGISTRY }}/${{ github.repository }}/app:${{ github.sha }} diff --git a/deployment/services/app.ts b/deployment/services/app.ts index 740adb8fd..28a690d80 100644 --- a/deployment/services/app.ts +++ b/deployment/services/app.ts @@ -62,28 +62,22 @@ export function deployApp({ env: [ { name: 'DEPLOYED_DNS', value: deploymentEnv.DEPLOYED_DNS }, { name: 'NODE_ENV', value: 'production' }, - { name: 'ENVIRONMENT', value: deploymentEnv.ENVIRONMENT }, { - name: 'NEXT_PUBLIC_ENVIRONMENT', + name: 'ENVIRONMENT', value: deploymentEnv.ENVIRONMENT, }, { name: 'RELEASE', value: appRelease, }, - { - name: 'NEXT_PUBLIC_RELEASE', - value: appRelease, - }, { name: 'SENTRY_DSN', value: commonEnv.SENTRY_DSN }, - { name: 'NEXT_PUBLIC_SENTRY_DSN', value: commonEnv.SENTRY_DSN }, { name: 'SENTRY_ENABLED', value: commonEnv.SENTRY_ENABLED }, { name: 'GRAPHQL_ENDPOINT', value: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`), }, { - name: 'NEXT_PUBLIC_APP_BASE_URL', + name: 'APP_BASE_URL', value: `https://${deploymentEnv.DEPLOYED_DNS}/`, }, { @@ -99,6 +93,26 @@ export function deployApp({ value: githubAppConfig.require('name'), }, + { + name: 'STRIPE_PUBLIC_KEY', + value: appEnv.STRIPE_PUBLIC_KEY, + }, + + { + name: 'GA_TRACKING_ID', + value: appEnv.GA_TRACKING_ID, + }, + + { + name: 'CRISP_WEBSITE_ID', + value: appEnv.CRISP_WEBSITE_ID, + }, + + { + name: 'DOCS_URL', + value: appEnv.DOCS_URL, + }, + // // AUTH // @@ -146,7 +160,7 @@ export function deployApp({ }, // GitHub { - name: 'NEXT_PUBLIC_AUTH_GITHUB', + name: 'AUTH_GITHUB', value: '1', }, { @@ -159,7 +173,7 @@ export function deployApp({ }, // Google { - name: 'NEXT_PUBLIC_AUTH_GOOGLE', + name: 'AUTH_GOOGLE', value: '1', }, { diff --git a/packages/web/app/.env.template b/packages/web/app/.env.template index d45336526..fe63729f0 100644 --- a/packages/web/app/.env.template +++ b/packages/web/app/.env.template @@ -1,6 +1,5 @@ GRAPHQL_ENDPOINT="http://localhost:4000/graphql" APP_BASE_URL="http://localhost:3000" -NEXT_PUBLIC_APP_BASE_URL="http://localhost:3000" # Supertokens @@ -10,12 +9,12 @@ SUPERTOKENS_API_KEY="bubatzbieber6942096420" # Auth Provider ## Enable GitHub login -NEXT_PUBLIC_AUTH_GITHUB=1 +AUTH_GITHUB=1 AUTH_GITHUB_CLIENT_ID="" AUTH_GITHUB_CLIENT_SECRET="" ## Enable Google login -NEXT_PUBLIC_AUTH_GOOGLE=1 +AUTH_GOOGLE=1 AUTH_GOOGLE_CLIENT_ID="" AUTH_GOOGLE_CLIENT_SECRET="" @@ -38,7 +37,7 @@ SLACK_CLIENT_SECRET="" GITHUB_APP_NAME="" # Stripe -NEXT_PUBLIC_STRIPE_PUBLIC_KEY="" +STRIPE_PUBLIC_KEY="" # Emails Service diff --git a/packages/web/app/Dockerfile b/packages/web/app/Dockerfile new file mode 100644 index 000000000..6735824a9 --- /dev/null +++ b/packages/web/app/Dockerfile @@ -0,0 +1,24 @@ +FROM node:16-slim as install + +WORKDIR /usr/src/app + +COPY dist/ /usr/src/app/ + +ENV NODE_ENV production + +# DANGER: there is no lockfile :) +# in the future this should be improved... + +RUN npm install --legacy-peer-deps + +FROM node:16-slim as app + +WORKDIR /usr/src/app + +COPY --from=install /usr/src/app/ /usr/src/app/ + +ENV ENVIRONMENT production +ENV RELEASE ${RELEASE} +ENV PORT 3000 + +CMD ["node", "index.js"] diff --git a/packages/web/app/README.md b/packages/web/app/README.md new file mode 100644 index 000000000..26f428cdc --- /dev/null +++ b/packages/web/app/README.md @@ -0,0 +1,63 @@ +# `@hive/app` + +The Hive application as seen on https://app.graphql-hive.com/. + +## Configuration + +The following environment variables configure the application. + +| Name | Required | Description | Example Value | +| ---------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| `APP_BASE_URL` | **Yes** | The base url of the app, | `https://app.graphql-hive.com` | +| `GRAPHQL_ENDPOINT` | **Yes** | The endpoint of the Hive GraphQL API. | `http://127.0.0.1:4000/graphql` | +| `EMAILS_ENDPOINT` | **Yes** | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` | +| `SUPERTOKENS_CONNECTION_URI` | **Yes** | The URI of the SuperTokens instance. | `http://127.0.0.1:3567` | +| `SUPERTOKENS_API_KEY` | **Yes** | The API KEY of the SuperTokens instance. | `iliketurtlesandicannotlie` | +| `SLACK_CLIENT_ID` | **Yes** | The Slack client ID. | `g6aff8102efda5e1d12e` | +| `SLACK_CLIENT_SECRET` | **Yes** | The Slack client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `GITHUB_APP_NAME` | **Yes** | The GitHub App name. | `graphql-hive-self-hosted` | +| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) | +| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` | +| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `AUTH_GOOGLE` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) | +| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` | +| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | +| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | +| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | +| `SENTRY_ENABLED` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | +| `DOCS_URL` | No | The URL of the Hive Docs | `https://docs.graphql-hive.com` | +| `NODE_ENV` | No | The `NODE_ENV` value. | `production` | +| `GA_TRACKING_ID` | No | The token for Google Analytics in order to track user actions. | `g6aff8102efda5e1d12e` | +| `CRISP_WEBSITE_ID` | No | The Crisp Website ID | `g6aff8102efda5e1d12e` | + +## Hive Hosted Configuration + +This is only important if you are hosting Hive for getting 💰. + +### Payments + +| Name | Required | Description | Example Value | +| ------------------- | -------- | ---------------------- | ---------------------- | +| `STRIPE_PUBLIC_KEY` | No | The Stripe Public Key. | `g6aff8102efda5e1d12e` | + +### Legacy Auth0 Configuration + +If you are not self-hosting GraphQL Hive, you can ignore this section. It is only required for the legacy Auth0 compatibility layer. + +| Name | Required | Description | Example Value | +| ----------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| `AUTH_LEGACY_AUTH0` | No | Whether the legacy Auth0 import is enabled. | `1` (enabled) or `0` (disabled) | +| `AUTH_LEGACY_AUTH0_CLIENT_ID` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 client ID. | `rDSpExxD8sfqlpF1kbxxLkMNYI2Sxxx` | +| `AUTH_LEGACY_AUTH0_CLIENT_SECRET` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 client secret. | `e43f156xx54en2b56117dc4abc4491dxxbb6b187` | +| `AUTH_LEGACY_AUTH0_ISSUER_BASE_URL` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 issuer base url. | `https://your-project.us.auth0.com` | +| `AUTH_LEGACY_AUTH0_AUDIENCE` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 audience | `https://your-project.us.auth0.com/api/v2/` | +| `AUTH_LEGACY_AUTH0_INTERNAL_API_ENDPOINT` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The internal endpoint for importing Auth0 accounts. (**Note:** This route is within the GraphQL service.) | `http://127.0.0.1:4000/__legacy` | +| `AUTH_LEGACY_AUTH0_INTERNAL_API_KEY` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The internal endpoint key. | `iliketurtles` | + +### Building the Docker Image + +**Prerequisites:** Make sure you built the mono-repository using `yarn build`. + +```bash +docker build . --build-arg RELEASE=stable-main -t graphql-hive/app +``` diff --git a/packages/web/app/modules.d.ts b/packages/web/app/modules.d.ts index 6ffb44bd5..ba0f652aa 100644 --- a/packages/web/app/modules.d.ts +++ b/packages/web/app/modules.d.ts @@ -10,9 +10,26 @@ declare module '@n1ru4l/react-time-ago' { declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_APP_BASE_URL: string; + APP_BASE_URL: string; GITHUB_APP_NAME: string; GRAPHQL_ENDPOINT: string; SUPERTOKENS_CONNECTION_URI: string; } } + +// eslint-disable-next-line no-var +declare var __ENV__: + | undefined + | { + APP_BASE_URL: string; + DOCS_URL: string | undefined; + STRIPE_PUBLIC_KEY: string | undefined; + AUTH_GITHUB: string | undefined; + AUTH_GOOGLE: string | undefined; + GA_TRACKING_ID: string | undefined; + CRISP_WEBSITE_ID: string | undefined; + SENTRY_DSN: string | undefined; + RELEASE: string | undefined; + ENVIRONMENT: string | undefined; + SENTRY_ENABLED: string | undefined; + }; diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx index 25a167ee8..27cc26f6b 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx @@ -18,6 +18,7 @@ import { ProjectFieldsFragment, TargetFieldsFragment, } from '@/graphql'; +import { getDocsUrl } from '@/lib/docs-url'; function floorDate(date: Date): Date { const time = 1000 * 60; @@ -144,7 +145,7 @@ const OperationsViewGate = ({ ) } diff --git a/packages/web/app/pages/[orgId]/[projectId]/index.tsx b/packages/web/app/pages/[orgId]/[projectId]/index.tsx index e090259c4..6d6018c13 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/index.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/index.tsx @@ -8,6 +8,7 @@ import { ProjectLayout } from '@/components/layouts'; import { Activities, Badge, Button, Card, DropdownMenu, EmptyList, Heading, TimeAgo, Title } from '@/components/v2'; import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon'; import { TargetQuery, TargetsDocument, VersionsDocument } from '@/graphql'; +import { getDocsUrl } from '@/lib/docs-url'; import { useClipboard } from '@/lib/hooks/use-clipboard'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; @@ -109,7 +110,7 @@ const Page = () => { ) : ( targets?.nodes.map(target => ) diff --git a/packages/web/app/pages/[orgId]/index.tsx b/packages/web/app/pages/[orgId]/index.tsx index 5a87cee12..5368cae06 100644 --- a/packages/web/app/pages/[orgId]/index.tsx +++ b/packages/web/app/pages/[orgId]/index.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react'; +import React, { ReactElement } from 'react'; import NextLink from 'next/link'; import { onlyText } from 'react-children-utilities'; import { useQuery } from 'urql'; @@ -9,6 +9,7 @@ import { Activities, Button, Card, DropdownMenu, EmptyList, Heading, Skeleton, T import { getActivity } from '@/components/v2/activities'; import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon'; import { ProjectActivitiesDocument, ProjectsWithTargetsDocument, ProjectsWithTargetsQuery } from '@/graphql'; +import { getDocsUrl } from '@/lib/docs-url'; import { fixDuplicatedFragments } from '@/lib/graphql'; import { useClipboard } from '@/lib/hooks/use-clipboard'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; @@ -94,6 +95,7 @@ function ProjectsPage(): ReactElement { }, }, }); + return ( <> @@ -106,7 +108,7 @@ function ProjectsPage(): ReactElement { <EmptyList title="Hive is waiting for your first project" description='You can create a project by clicking the "Create Project" button' - docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/projects`} + docsUrl={getDocsUrl(`/get-started/projects`)} /> ) : ( <div className="grid grid-cols-2 gap-5"> diff --git a/packages/web/app/pages/[orgId]/subscription/index.tsx b/packages/web/app/pages/[orgId]/subscription/index.tsx index bcbc7cb36..a7f55a74d 100644 --- a/packages/web/app/pages/[orgId]/subscription/index.tsx +++ b/packages/web/app/pages/[orgId]/subscription/index.tsx @@ -13,6 +13,7 @@ import { OrganizationUsageEstimationView } from '@/components/organization/Usage import { Card, Heading, Tabs, Title } from '@/components/v2'; import { OrganizationFieldsFragment, OrgBillingInfoFieldsFragment, OrgRateLimitFieldsFragment } from '@/graphql'; import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization'; +import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key'; const DateFormatter = Intl.DateTimeFormat('en-US', { month: 'short', @@ -110,6 +111,22 @@ function SubscriptionPage(): ReactElement { ); } -export const getServerSideProps = withSessionProtection(); +export const getServerSideProps = withSessionProtection(async context => { + /** + * If Strive is not enabled we redirect the user to the organization. + */ + const isStripeEnabled = getIsStripeEnabled(); + if (isStripeEnabled === false) { + const parts = `${context.resolvedUrl}`.split('/'); + parts.pop(); + return { + redirect: { + destination: `${parts.join('/')}`, + permanent: false, + }, + }; + } + return { props: {} }; +}); export default authenticated(SubscriptionPage); diff --git a/packages/web/app/pages/_document.tsx b/packages/web/app/pages/_document.tsx index ae8c373dc..17a5082a5 100644 --- a/packages/web/app/pages/_document.tsx +++ b/packages/web/app/pages/_document.tsx @@ -2,16 +2,50 @@ import 'regenerator-runtime/runtime'; import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'; import { extractCritical } from '@emotion/server'; -export default class MyDocument extends Document { +type FrontendEnvironment = { + APP_BASE_URL: string | undefined; + DOCS_URL: string | undefined; + STRIPE_PUBLIC_KEY: string | undefined; + AUTH_GITHUB: string | undefined; + AUTH_GOOGLE: string | undefined; + GA_TRACKING_ID: string | undefined; + CRISP_WEBSITE_ID: string | undefined; + SENTRY_DSN: string | undefined; + RELEASE: string | undefined; + ENVIRONMENT: string | undefined; + SENTRY_ENABLED: string | undefined; +}; + +export default class MyDocument extends Document<{ ids: Array<string>; css: string; __ENV__: FrontendEnvironment }> { static async getInitialProps(ctx: DocumentContext) { const initialProps = await Document.getInitialProps(ctx); const page = await ctx.renderPage(); const styles = extractCritical(page.html); - return { ...initialProps, ...page, ...styles }; + + const __ENV__: FrontendEnvironment = { + APP_BASE_URL: process.env['APP_BASE_URL'], + DOCS_URL: process.env['DOCS_URL'], + STRIPE_PUBLIC_KEY: process.env['STRIPE_PUBLIC_KEY'], + AUTH_GITHUB: process.env['AUTH_GITHUB'], + AUTH_GOOGLE: process.env['AUTH_GOOGLE'], + GA_TRACKING_ID: process.env['GA_TRACKING_ID'], + CRISP_WEBSITE_ID: process.env['CRISP_WEBSITE_ID'], + SENTRY_DSN: process.env['SENTRY_DSN'], + RELEASE: process.env['RELEASE'], + ENVIRONMENT: process.env['ENVIRONMENT'], + SENTRY_ENABLED: process.env['SENTRY_ENABLED'], + }; + + return { + ...initialProps, + ...page, + ...styles, + __ENV__, + }; } render() { - const { ids, css } = this.props as any; + const { ids, css } = this.props; return ( <Html className="dark"> @@ -36,6 +70,12 @@ export default class MyDocument extends Document { id="force-dark-mode" dangerouslySetInnerHTML={{ __html: "localStorage['chakra-ui-color-mode'] = 'dark';" }} /> + <script + type="module" + dangerouslySetInnerHTML={{ + __html: `globalThis["__ENV__"] = ${JSON.stringify((this.props as any).__ENV__)}`, + }} + /> </Head> <body className="bg-transparent font-sans text-white"> <Main /> diff --git a/packages/web/app/pages/api/auth/[[...path]].ts b/packages/web/app/pages/api/auth/[[...path]].ts index 3af9bc955..53f984d02 100644 --- a/packages/web/app/pages/api/auth/[[...path]].ts +++ b/packages/web/app/pages/api/auth/[[...path]].ts @@ -15,7 +15,7 @@ export default async function superTokens(req: NextApiRequest & Request, res: Ne // NOTE: We need CORS only if we are querying the APIs from a different origin await NextCors(req, res, { methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], - origin: process.env['NEXT_PUBLIC_APP_BASE_URL'], + origin: process.env['APP_BASE_URL'], credentials: true, allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()], }); diff --git a/packages/web/app/pages/api/github/callback.ts b/packages/web/app/pages/api/github/callback.ts index e4514f89e..47e66612e 100644 --- a/packages/web/app/pages/api/github/callback.ts +++ b/packages/web/app/pages/api/github/callback.ts @@ -10,7 +10,7 @@ export async function ensureGithubIntegration( ) { const { orgId, installationId } = input; await graphql({ - url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, + url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, headers: { ...req.headers, 'content-type': 'application/json', diff --git a/packages/web/app/pages/api/github/connect/[orgId].ts b/packages/web/app/pages/api/github/connect/[orgId].ts index e65bf5af3..73b005fb4 100644 --- a/packages/web/app/pages/api/github/connect/[orgId].ts +++ b/packages/web/app/pages/api/github/connect/[orgId].ts @@ -7,7 +7,7 @@ export default async function githubConnectOrg(req: NextApiRequest, res: NextApi const url = `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new`; - const redirectUrl = `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/github/callback`; + const redirectUrl = `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/github/callback`; res.redirect(`${url}?state=${orgId}&redirect_url=${redirectUrl}`); } diff --git a/packages/web/app/pages/api/github/setup-callback.ts b/packages/web/app/pages/api/github/setup-callback.ts index 0643f277b..80281b953 100644 --- a/packages/web/app/pages/api/github/setup-callback.ts +++ b/packages/web/app/pages/api/github/setup-callback.ts @@ -18,7 +18,7 @@ export default async function githubSetupCallback(req: NextApiRequest, res: Next cleanId: string; }; }>({ - url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, + url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, headers: { ...req.headers, 'content-type': 'application/json', diff --git a/packages/web/app/pages/api/slack/callback.ts b/packages/web/app/pages/api/slack/callback.ts index d4ed032e9..4820cfc25 100644 --- a/packages/web/app/pages/api/slack/callback.ts +++ b/packages/web/app/pages/api/slack/callback.ts @@ -32,7 +32,7 @@ export default async function slackCallback(req: NextApiRequest, res: NextApiRes const token = slackResponse.access_token; await graphql({ - url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, + url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`, headers: { ...req.headers, 'content-type': 'application/json', diff --git a/packages/web/app/pages/api/slack/connect/[orgId].ts b/packages/web/app/pages/api/slack/connect/[orgId].ts index e46b46472..7d915cb58 100644 --- a/packages/web/app/pages/api/slack/connect/[orgId].ts +++ b/packages/web/app/pages/api/slack/connect/[orgId].ts @@ -7,7 +7,7 @@ export default async function slackConnectOrg(req: NextApiRequest, res: NextApiR const slackUrl = `https://slack.com/oauth/v2/authorize?scope=incoming-webhook,chat:write,chat:write.public,commands&client_id=${process .env.SLACK_CLIENT_ID!}`; - const redirectUrl = `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/slack/callback`; + const redirectUrl = `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/slack/callback`; res.redirect(`${slackUrl}&state=${orgId}&redirect_uri=${redirectUrl}`); } diff --git a/packages/web/app/sentry.config.ts b/packages/web/app/sentry.config.ts index 047644e81..9e6ab4bda 100644 --- a/packages/web/app/sentry.config.ts +++ b/packages/web/app/sentry.config.ts @@ -1,14 +1,14 @@ import { init } from '@sentry/nextjs'; -const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; -const RELEASE = process.env.RELEASE || process.env.NEXT_PUBLIC_RELEASE; -const ENVIRONMENT = process.env.ENVIRONMENT || process.env.NEXT_PUBLIC_ENVIRONMENT; -const SENTRY_ENABLED = process.env.SENTRY_ENABLED || process.env.NEXT_PUBLIC_SENTRY_ENABLED; +const SENTRY_DSN = globalThis.process?.env['SENTRY_DSN'] ?? globalThis['__ENV__']?.['SENTRY_DSN']; +const RELEASE = globalThis.process?.env['RELEASE'] ?? globalThis['__ENV__']?.['RELEASE']; +const ENVIRONMENT = globalThis.process?.env['ENVIRONMENT'] ?? globalThis['__ENV__']?.['ENVIRONMENT']; +const SENTRY_ENABLED = globalThis.process?.env['SENTRY_ENABLED'] ?? globalThis['__ENV__']?.['SENTRY_ENABLED']; export const config: Parameters<typeof init>[0] = { serverName: 'app', - enabled: String(SENTRY_ENABLED) === '1', - environment: ENVIRONMENT || 'local', - release: RELEASE || 'local', + enabled: SENTRY_ENABLED === '1', + environment: ENVIRONMENT ?? 'local', + release: RELEASE ?? 'local', dsn: SENTRY_DSN, }; diff --git a/packages/web/app/src/components/common/Navigation.tsx b/packages/web/app/src/components/common/Navigation.tsx index 0d1b7d042..a8799d51a 100644 --- a/packages/web/app/src/components/common/Navigation.tsx +++ b/packages/web/app/src/components/common/Navigation.tsx @@ -21,6 +21,7 @@ import { Logo } from './Logo'; import { Feedback } from './Feedback'; import { UserSettings } from './UserSettings'; import ThemeButton from './ThemeButton'; +import { getDocsUrl } from '@/lib/docs-url'; export interface NavigationItem { label: string; @@ -121,6 +122,8 @@ export function Navigation() { const me = meQuery.data?.me; + const docsUrl = getDocsUrl(); + return ( <nav tw="bg-white shadow-md dark:bg-gray-900 z-10"> <div tw="mx-auto px-2 sm:px-6 lg:px-8"> @@ -149,11 +152,13 @@ export function Navigation() { </div> </div> </div> - <div tw="flex flex-row items-center space-x-4"> - <ChakraLink tw="text-sm dark:text-gray-200" href={process.env.NEXT_PUBLIC_DOCS_LINK}> - Documentation - </ChakraLink> - </div> + {docsUrl ? ( + <div tw="flex flex-row items-center space-x-4"> + <ChakraLink tw="text-sm dark:text-gray-200" href={docsUrl}> + Documentation + </ChakraLink> + </div> + ) : null} <Divider orientation="vertical" tw="height[20px] ml-8 mr-3" /> <div tw="inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:pr-0"> <div tw="ml-3 relative"> diff --git a/packages/web/app/src/components/get-started/wizard.tsx b/packages/web/app/src/components/get-started/wizard.tsx index 074d23539..75c410280 100644 --- a/packages/web/app/src/components/get-started/wizard.tsx +++ b/packages/web/app/src/components/get-started/wizard.tsx @@ -12,6 +12,7 @@ import { import clsx from 'clsx'; import { OrganizationType } from '@/graphql'; import { gql, DocumentType } from 'urql'; +import { getDocsUrl } from '@/lib/docs-url'; const GetStartedWizard_GetStartedProgress = gql(/* GraphQL */ ` fragment GetStartedWizard_GetStartedProgress on OrganizationGetStarted { @@ -104,37 +105,25 @@ function GetStartedWizard({ <DrawerBody> <p>Complete these steps to experience the full power of GraphQL Hive</p> <div className="mt-4 flex flex-col divide-y-2 divide-gray-900"> - <Task link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/projects`} completed={tasks.creatingProject}> + <Task link={getDocsUrl(`/get-started/projects`)} completed={tasks.creatingProject}> Create a project </Task> - <Task - link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/publish-schema`} - completed={tasks.publishingSchema} - > + <Task link={getDocsUrl(`/features/publish-schema`)} completed={tasks.publishingSchema}> Publish a schema </Task> - <Task - link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/checking-schema`} - completed={tasks.checkingSchema} - > + <Task link={getDocsUrl(`/features/checking-schema`)} completed={tasks.checkingSchema}> Check a schema </Task> {'invitingMembers' in tasks && typeof tasks.invitingMembers === 'boolean' ? ( - <Task - link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/organizations#members`} - completed={tasks.invitingMembers} - > + <Task link={getDocsUrl(`/get-started/organizations#members`)} completed={tasks.invitingMembers}> Invite members </Task> ) : null} - <Task - link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/monitoring`} - completed={tasks.reportingOperations} - > + <Task link={getDocsUrl(`/features/monitoring`)} completed={tasks.reportingOperations}> Report operations </Task> <Task - link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/checking-schema#with-usage-enabled`} + link={getDocsUrl(`/features/checking-schema#with-usage-enabled`)} completed={tasks.enablingUsageBasedBreakingChanges} > Enable usage-based breaking changes @@ -152,11 +141,11 @@ function Task({ link, }: React.PropsWithChildren<{ completed: boolean; - link: string; + link: string | null; }>) { return ( <a - href={link} + href={link ?? undefined} target="_blank" rel="noreferrer" className={clsx('flex flex-row items-center gap-4 p-3 text-left', completed ? 'opacity-50' : 'hover:opacity-80')} diff --git a/packages/web/app/src/components/layouts/organization.tsx b/packages/web/app/src/components/layouts/organization.tsx index f74df38c0..55aac66fc 100644 --- a/packages/web/app/src/components/layouts/organization.tsx +++ b/packages/web/app/src/components/layouts/organization.tsx @@ -17,6 +17,7 @@ import { useRouteSelector } from '@/lib/hooks/use-route-selector'; import { canAccessOrganization, OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization'; import cookies from 'js-cookie'; import { LAST_VISITED_ORG_KEY } from '@/constants'; +import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key'; enum TabValue { Overview = 'overview', @@ -146,7 +147,7 @@ export function OrganizationLayout({ </Tabs.Trigger> </NextLink> )} - {canAccessOrganization(OrganizationAccessScope.Settings, me) && ( + {getIsStripeEnabled() && canAccessOrganization(OrganizationAccessScope.Settings, me) && ( <NextLink passHref href={`/${orgId}/${TabValue.Subscription}`}> <Tabs.Trigger value={TabValue.Subscription} asChild> <a>Subscription</a> diff --git a/packages/web/app/src/components/v2/empty-list.tsx b/packages/web/app/src/components/v2/empty-list.tsx index a062d9965..77d537b95 100644 --- a/packages/web/app/src/components/v2/empty-list.tsx +++ b/packages/web/app/src/components/v2/empty-list.tsx @@ -2,6 +2,7 @@ import { ReactElement } from 'react'; import Image from 'next/image'; import { Card, Heading, Link } from '@/components/v2/index'; +import { getDocsUrl } from '@/lib/docs-url'; import magnifier from '../../../public/images/figures/magnifier.svg'; export const EmptyList = ({ @@ -11,16 +12,18 @@ export const EmptyList = ({ }: { title: string; description: string; - docsUrl: string; + docsUrl: string | null; }): ReactElement => { return ( <Card className="flex grow flex-col items-center gap-y-2"> <Image src={magnifier} alt="Magnifier illustration" width="200" height="200" className="drag-none" /> <Heading>{title}</Heading> <span className="text-center text-sm font-medium text-gray-500">{description}</span> - <Link variant="primary" href={docsUrl} target="_blank" rel="noreferrer" className="my-5"> - Read about it in the documentation - </Link> + {docsUrl === null ? null : ( + <Link variant="primary" href={docsUrl} target="_blank" rel="noreferrer" className="my-5"> + Read about it in the documentation + </Link> + )} </Card> ); }; @@ -29,6 +32,6 @@ export const noSchema = ( <EmptyList title="Hive is waiting for your first schema" description="You can publish a schema with Hive CLI and Hive Client" - docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/publish-schema`} + docsUrl={getDocsUrl(`/features/publish-schema`)} /> ); diff --git a/packages/web/app/src/components/v2/header.tsx b/packages/web/app/src/components/v2/header.tsx index dd4afea13..151d0443c 100644 --- a/packages/web/app/src/components/v2/header.tsx +++ b/packages/web/app/src/components/v2/header.tsx @@ -19,6 +19,7 @@ import { } from '@/components/v2/icon'; import { CreateOrganizationModal } from '@/components/v2/modals'; import { MeDocument, OrganizationsDocument, OrganizationsQuery, OrganizationType } from '@/graphql'; +import { getDocsUrl } from '@/lib/docs-url'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; type DropdownOrganization = OrganizationsQuery['organizations']['nodes']; @@ -72,6 +73,7 @@ export const Header = (): ReactElement => { }; }, [isOpaque]); + const docsUrl = getDocsUrl(); return ( <header className={clsx( @@ -142,12 +144,14 @@ export const Header = (): ReactElement => { </DropdownMenu.Item> </a> </NextLink> - <DropdownMenu.Item asChild> - <a href={process.env.NEXT_PUBLIC_DOCS_LINK} target="_blank" rel="noreferrer"> - <FileTextIcon className="h-5 w-5" /> - Documentation - </a> - </DropdownMenu.Item> + {docsUrl ? ( + <DropdownMenu.Item asChild> + <a href={docsUrl} target="_blank" rel="noreferrer"> + <FileTextIcon className="h-5 w-5" /> + Documentation + </a> + </DropdownMenu.Item> + ) : null} <DropdownMenu.Item asChild> <a href="https://status.graphql-hive.com" target="_blank" rel="noreferrer"> <AlertTriangleIcon className="h-5 w-5" /> diff --git a/packages/web/app/src/components/v2/modals/connect-lab.tsx b/packages/web/app/src/components/v2/modals/connect-lab.tsx index 006c945a5..04789eca6 100644 --- a/packages/web/app/src/components/v2/modals/connect-lab.tsx +++ b/packages/web/app/src/components/v2/modals/connect-lab.tsx @@ -1,6 +1,7 @@ import { ReactElement } from 'react'; import { Button, CopyValue, Heading, Link, Modal, Tag } from '@/components/v2'; +import { getDocsUrl } from '@/lib/docs-url'; export const ConnectLabModal = ({ isOpen, @@ -11,6 +12,8 @@ export const ConnectLabModal = ({ toggleModalOpen: () => void; endpoint: string; }): ReactElement => { + const docsUrl = getDocsUrl('/features/tokens'); + return ( <Modal open={isOpen} onOpenChange={toggleModalOpen} className="flex w-[650px] flex-col gap-5"> <Heading className="text-center">Connect to Lab</Heading> @@ -22,23 +25,13 @@ export const ConnectLabModal = ({ <span className="text-sm text-gray-500">To authenticate, use the following HTTP headers:</span> <Tag> X-Hive-Key:{' '} - <Link - variant="secondary" - target="_blank" - rel="noreferrer" - href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/tokens`} - > + <Link variant="secondary" target="_blank" rel="noreferrer" href={docsUrl ?? undefined}> YOUR_TOKEN_HERE </Link> </Tag> <p className="text-sm text-gray-500"> Read the{' '} - <Link - variant="primary" - target="_blank" - rel="noreferrer" - href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/tokens`} - > + <Link variant="primary" target="_blank" rel="noreferrer" href={docsUrl ?? undefined}> Managing Tokens </Link>{' '} chapter in our documentation. diff --git a/packages/web/app/src/components/v2/modals/connect-schema.tsx b/packages/web/app/src/components/v2/modals/connect-schema.tsx index eb7f8669a..5ab93304b 100644 --- a/packages/web/app/src/components/v2/modals/connect-schema.tsx +++ b/packages/web/app/src/components/v2/modals/connect-schema.tsx @@ -4,6 +4,7 @@ import { useMutation, useQuery } from 'urql'; import { Button, CopyValue, Heading, Link, Modal, Tag } from '@/components/v2'; import { CreateCdnTokenDocument, ProjectDocument, ProjectType } from '@/graphql'; +import { getDocsUrl } from '@/lib/docs-url'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; const taxonomy = { @@ -83,7 +84,7 @@ export const ConnectSchemaModal = ({ variant="primary" target="_blank" rel="noreferrer" - href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/registry-usage#apollo-federation`} + href={getDocsUrl(`/features/registry-usage#apollo-federation`) ?? undefined} > Using the Registry with a Apollo Gateway </Link>{' '} diff --git a/packages/web/app/src/config/app-info.ts b/packages/web/app/src/config/app-info.ts index 447226cb9..e7a570b43 100644 --- a/packages/web/app/src/config/app-info.ts +++ b/packages/web/app/src/config/app-info.ts @@ -1,8 +1,19 @@ -export const appInfo = { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: 'GraphQL Hive', - apiDomain: process.env['NEXT_PUBLIC_APP_BASE_URL'], - websiteDomain: process.env['NEXT_PUBLIC_APP_BASE_URL'], - apiBasePath: '/api/auth', - websiteBasePath: '/auth', +function throwException(msg: string) { + throw new Error(msg); +} + +export const appInfo = () => { + const appBaseUrl = + globalThis.process?.env?.['APP_BASE_URL'] ?? + globalThis?.['__ENV__']?.['APP_BASE_URL'] ?? + throwException('APP_BASE_URL is not defined'); + + return { + // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo + appName: 'GraphQL Hive', + apiDomain: appBaseUrl, + websiteDomain: appBaseUrl, + apiBasePath: '/api/auth', + websiteBasePath: '/auth', + }; }; diff --git a/packages/web/app/src/config/backend-config.ts b/packages/web/app/src/config/backend-config.ts index c30dd8c3b..ee2a79f71 100644 --- a/packages/web/app/src/config/backend-config.ts +++ b/packages/web/app/src/config/backend-config.ts @@ -35,10 +35,10 @@ type LegacyAuth0ConfigEnabled = zod.TypeOf<typeof LegacyAuth0ConfigEnabledModel> const GitHubConfigModel = zod.union([ zod.object({ - NEXT_PUBLIC_AUTH_GITHUB: zod.union([zod.void(), zod.literal('0')]), + AUTH_GITHUB: zod.union([zod.void(), zod.literal('0')]), }), zod.object({ - NEXT_PUBLIC_AUTH_GITHUB: zod.literal('1'), + AUTH_GITHUB: zod.literal('1'), AUTH_GITHUB_CLIENT_ID: zod.string(), AUTH_GITHUB_CLIENT_SECRET: zod.string(), }), @@ -46,10 +46,10 @@ const GitHubConfigModel = zod.union([ const GoogleConfigModel = zod.union([ zod.object({ - NEXT_PUBLIC_AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0')]), + AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0')]), }), zod.object({ - NEXT_PUBLIC_AUTH_GOOGLE: zod.literal('1'), + AUTH_GOOGLE: zod.literal('1'), AUTH_GOOGLE_CLIENT_ID: zod.string(), AUTH_GOOGLE_CLIENT_SECRET: zod.string(), }), @@ -72,7 +72,7 @@ export const backendConfig = (): TypeInput => { }); const providers: Array<TypeProvider> = []; - if (githubConfig['NEXT_PUBLIC_AUTH_GITHUB'] === '1') { + if (githubConfig['AUTH_GITHUB'] === '1') { providers.push( ThirdPartyEmailPasswordNode.Github({ clientId: githubConfig['AUTH_GITHUB_CLIENT_ID'], @@ -80,7 +80,7 @@ export const backendConfig = (): TypeInput => { }) ); } - if (googleConfig['NEXT_PUBLIC_AUTH_GOOGLE'] === '1') { + if (googleConfig['AUTH_GOOGLE'] === '1') { providers.push( ThirdPartyEmailPasswordNode.Google({ clientId: googleConfig['AUTH_GOOGLE_CLIENT_ID'], @@ -94,7 +94,7 @@ export const backendConfig = (): TypeInput => { connectionURI: superTokensConfig['SUPERTOKENS_CONNECTION_URI'], apiKey: superTokensConfig['SUPERTOKENS_API_KEY'], }, - appInfo, + appInfo: appInfo(), recipeList: [ ThirdPartyEmailPasswordNode.init({ providers, diff --git a/packages/web/app/src/config/frontend-config.ts b/packages/web/app/src/config/frontend-config.ts index 4eaaf2dff..133ff32bb 100644 --- a/packages/web/app/src/config/frontend-config.ts +++ b/packages/web/app/src/config/frontend-config.ts @@ -7,15 +7,15 @@ import { appInfo } from './app-info'; export const frontendConfig = () => { const providers: Array<Provider> = []; - if (process.env['NEXT_PUBLIC_AUTH_GITHUB'] === '1') { + if (globalThis['__ENV__']?.['AUTH_GITHUB'] === '1') { providers.push(ThirdPartyEmailPasswordReact.Github.init()); } - if (process.env['NEXT_PUBLIC_AUTH_GOOGLE'] === '1') { + if (globalThis['__ENV__']?.['AUTH_GOOGLE'] === '1') { providers.push(ThirdPartyEmailPasswordReact.Google.init()); } return { - appInfo, + appInfo: appInfo(), recipeList: [ ThirdPartyEmailPasswordReact.init({ signInAndUpFeature: { diff --git a/packages/web/app/src/constants.ts b/packages/web/app/src/constants.ts index 1e22e397f..f1436aed8 100644 --- a/packages/web/app/src/constants.ts +++ b/packages/web/app/src/constants.ts @@ -1,5 +1,6 @@ export const LAST_VISITED_ORG_KEY = 'lastVisitedOrganization'; -export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_TRACKING_ID; +export const GA_TRACKING_ID = globalThis.process?.env['GA_TRACKING_ID'] ?? globalThis['__ENV__']?.['GA_TRACKING_ID']; -export const CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID; +export const CRISP_WEBSITE_ID = + globalThis.process?.env['CRISP_WEBSITE_ID'] ?? globalThis['__ENV__']?.['CRISP_WEBSITE_ID']; diff --git a/packages/web/app/src/lib/billing/stripe-public-key.ts b/packages/web/app/src/lib/billing/stripe-public-key.ts new file mode 100644 index 000000000..aa08f7617 --- /dev/null +++ b/packages/web/app/src/lib/billing/stripe-public-key.ts @@ -0,0 +1,9 @@ +export const getStripePublicKey = () => { + const stripePublicUrl = globalThis.process?.env['STRIPE_PUBLIC_KEY'] ?? globalThis['__ENV__']?.['STRIPE_PUBLIC_KEY']; + if (!stripePublicUrl) { + return null; + } + return stripePublicUrl; +}; + +export const getIsStripeEnabled = () => !!getStripePublicKey(); diff --git a/packages/web/app/src/lib/billing/stripe.tsx b/packages/web/app/src/lib/billing/stripe.tsx index b42320bf4..9a34961d0 100644 --- a/packages/web/app/src/lib/billing/stripe.tsx +++ b/packages/web/app/src/lib/billing/stripe.tsx @@ -1,15 +1,20 @@ import { Elements as ElementsProvider } from '@stripe/react-stripe-js'; -import { loadStripe } from '@stripe/stripe-js'; import React from 'react'; - -const STRIPE_PUBLIC_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || null; - -const stripePromise$ = !STRIPE_PUBLIC_KEY ? null : loadStripe(STRIPE_PUBLIC_KEY); +import { getStripePublicKey } from './stripe-public-key'; +import { loadStripe } from '@stripe/stripe-js'; export const HiveStripeWrapper: React.FC<{}> = ({ children }) => { - if (STRIPE_PUBLIC_KEY === null || stripePromise$ === null) { + const [stripe] = React.useState(() => { + const stripePublicKey = getStripePublicKey(); + return stripePublicKey ? loadStripe(stripePublicKey) : null; + }); + + if (stripe === null) { return children as any; } - - return <ElementsProvider stripe={stripePromise$}>{children}</ElementsProvider>; + return ( + <React.Suspense fallback={() => <>{children}</>}> + <ElementsProvider stripe={stripe}>{children}</ElementsProvider> + </React.Suspense> + ); }; diff --git a/packages/web/app/src/lib/docs-url.ts b/packages/web/app/src/lib/docs-url.ts new file mode 100644 index 000000000..e24295ac9 --- /dev/null +++ b/packages/web/app/src/lib/docs-url.ts @@ -0,0 +1,7 @@ +export const getDocsUrl = (path = '') => { + const docsUrl = globalThis.process?.env['DOCS_URL'] ?? globalThis['__ENV__']?.['DOCS_URL']; + if (!docsUrl) { + return null; + } + return `${docsUrl}${path}`; +}; diff --git a/turbo.json b/turbo.json index 68a8c3f31..daa44aa6d 100644 --- a/turbo.json +++ b/turbo.json @@ -18,13 +18,7 @@ "outputs": ["dist/**"] }, "@hive/app#build": { - "dependsOn": [ - "^build", - "$NEXT_PUBLIC_DOCS_LINK", - "$NEXT_PUBLIC_CRISP_WEBSITE_ID", - "$NEXT_PUBLIC_GA_TRACKING_ID", - "$NEXT_PUBLIC_STRIPE_PUBLIC_KEY" - ], + "dependsOn": ["^build"], "outputs": ["dist/**"] }, "@hive/docs#build": {