From 6b487787539f68077d6a3ddacb00fa7a4be61e1d Mon Sep 17 00:00:00 2001 From: gbuomprisco Date: Sat, 22 Jun 2024 20:21:08 +0800 Subject: [PATCH] Add version updater feature Added a version updater component that frequently checks for updates to the app and alerts the user if necessary. This requires a new route, config changes, and additional UI resources. A new feature flag 'enableVersionUpdater' has been added in the feature-flags.config.ts file to toggle this feature. --- apps/web/app/version/route.ts | 38 +++++++ apps/web/components/root-providers.tsx | 7 ++ apps/web/config/feature-flags.config.ts | 8 ++ apps/web/public/locales/en/common.json | 4 + packages/ui/package.json | 4 +- packages/ui/src/makerkit/version-updater.tsx | 114 +++++++++++++++++++ pnpm-lock.yaml | 7 +- turbo/generators/templates/env/generator.ts | 105 ++++++++--------- 8 files changed, 232 insertions(+), 55 deletions(-) create mode 100644 apps/web/app/version/route.ts create mode 100644 packages/ui/src/makerkit/version-updater.tsx diff --git a/apps/web/app/version/route.ts b/apps/web/app/version/route.ts new file mode 100644 index 000000000..2237ea8b0 --- /dev/null +++ b/apps/web/app/version/route.ts @@ -0,0 +1,38 @@ +/** + * We force it to static because we want to cache for as long as the build is live. + */ +export const dynamic = 'force-static'; + +// please provide your own implementation +// if you're not using Vercel or Cloudflare Pages +const KNOWN_GIT_ENV_VARS = [ + 'CF_PAGES_COMMIT_SHA', + 'VERCEL_GIT_COMMIT_SHA', + 'GIT_HASH', +]; + +export const GET = async () => { + const currentGitHash = await getGitHash(); + + return new Response(currentGitHash, { + headers: { + 'content-type': 'text/plain', + }, + }); +}; + +function getGitHash() { + for (const envVar of KNOWN_GIT_ENV_VARS) { + if (process.env[envVar]) { + return process.env[envVar]; + } + } + + return getHashFromProcess(); +} + +async function getHashFromProcess() { + const { execSync } = await import('child_process'); + + return execSync('git log --pretty=format:"%h" -n1').toString().trim(); +} diff --git a/apps/web/components/root-providers.tsx b/apps/web/components/root-providers.tsx index 429e78f5a..55df9db1d 100644 --- a/apps/web/components/root-providers.tsx +++ b/apps/web/components/root-providers.tsx @@ -9,9 +9,12 @@ import { CaptchaProvider } from '@kit/auth/captcha/client'; import { I18nProvider } from '@kit/i18n/provider'; import { MonitoringProvider } from '@kit/monitoring/components'; import { useAuthChangeListener } from '@kit/supabase/hooks/use-auth-change-listener'; +import { If } from '@kit/ui/if'; +import { VersionUpdater } from '@kit/ui/version-updater'; import appConfig from '~/config/app.config'; import authConfig from '~/config/auth.config'; +import featuresFlagConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; import { i18nResolver } from '~/lib/i18n/i18n.resolver'; import { getI18nSettings } from '~/lib/i18n/i18n.settings'; @@ -62,6 +65,10 @@ export function RootProviders({ + + + + diff --git a/apps/web/config/feature-flags.config.ts b/apps/web/config/feature-flags.config.ts index 39d3a4415..ea0a50574 100644 --- a/apps/web/config/feature-flags.config.ts +++ b/apps/web/config/feature-flags.config.ts @@ -50,6 +50,10 @@ const FeatureFlagsSchema = z.object({ description: 'Enable realtime for the notifications functionality', required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS', }), + enableVersionUpdater: z.boolean({ + description: 'Enable version updater', + required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER', + }), }); const featuresFlagConfig = FeatureFlagsSchema.parse({ @@ -91,6 +95,10 @@ const featuresFlagConfig = FeatureFlagsSchema.parse({ process.env.NEXT_PUBLIC_REALTIME_NOTIFICATIONS, false, ), + enableVersionUpdater: getBoolean( + process.env.NEXT_PUBLIC_ENABLE_VERSION_UPDATER, + false, + ), } satisfies z.infer); export default featuresFlagConfig; diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 228355fe9..16632219e 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -51,6 +51,10 @@ "notifications": "Notifications", "noNotifications": "No notifications", "justNow": "Just now", + "newVersionAvailable": "New version available", + "newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.", + "newVersionSubmitButton": "Reload and Update", + "back": "Back", "roles": { "owner": { "label": "Owner" diff --git a/packages/ui/package.json b/packages/ui/package.json index 57ebcde36..0d5341ade 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -41,6 +41,7 @@ "@kit/tailwind-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@radix-ui/react-icons": "^1.3.0", + "@tanstack/react-query": "5.45.1", "@tanstack/react-table": "^8.17.3", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", @@ -118,7 +119,8 @@ "./language-selector": "./src/makerkit/language-selector.tsx", "./stepper": "./src/makerkit/stepper.tsx", "./cookie-banner": "./src/makerkit/cookie-banner.tsx", - "./card-button": "./src/makerkit/card-button.tsx" + "./card-button": "./src/makerkit/card-button.tsx", + "./version-updater": "./src/makerkit/version-updater.tsx" }, "typesVersions": { "*": { diff --git a/packages/ui/src/makerkit/version-updater.tsx b/packages/ui/src/makerkit/version-updater.tsx new file mode 100644 index 000000000..8c5f1f9c7 --- /dev/null +++ b/packages/ui/src/makerkit/version-updater.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { useQuery } from '@tanstack/react-query'; + +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '../shadcn/alert-dialog'; +import { Button } from '../shadcn/button'; +import { Trans } from './trans'; + +/** + * Current version of the app that is running + */ +let version: string | null = null; + +/** + * Default interval time in seconds to check for new version + * By default, it is set to 120 seconds + */ +const DEFAULT_REFETCH_INTERVAL = 120; + +/** + * Default interval time in seconds to check for new version + */ +const VERSION_UPDATER_REFETCH_INTERVAL_SECONDS = process.env + .VERSION_UDPATER_REFETCH_INTERVAL_SECONDS; + +export function VersionUpdater(props: { intervalTimeInSecond?: number }) { + const { data } = useVersionUpdater(props); + const [dismissed, setDismissed] = useState(false); + const [showDialog, setShowDialog] = useState(false); + + useEffect(() => { + setShowDialog(data?.didChange ?? false); + }, [data?.didChange]); + + if (!data?.didChange || dismissed) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +function useVersionUpdater(props: { intervalTimeInSecond?: number } = {}) { + const interval = VERSION_UPDATER_REFETCH_INTERVAL_SECONDS + ? Number(VERSION_UPDATER_REFETCH_INTERVAL_SECONDS) + : DEFAULT_REFETCH_INTERVAL; + + const refetchInterval = (props.intervalTimeInSecond ?? interval) * 1000; + + // start fetching new version after half of the interval time + const staleTime = refetchInterval / 2; + + return useQuery({ + queryKey: ['version-updater'], + staleTime, + gcTime: refetchInterval, + refetchIntervalInBackground: true, + refetchInterval, + initialData: null, + queryFn: async () => { + const response = await fetch('/version'); + const currentVersion = await response.text(); + const oldVersion = version; + + version = currentVersion; + + const didChange = oldVersion !== null && currentVersion !== oldVersion; + + return { + currentVersion, + oldVersion, + didChange, + }; + }, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4404d9bbd..e68120f48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1038,7 +1038,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^8.11.0 - version: 8.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1) + version: 8.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1) devDependencies: '@kit/eslint-config': specifier: workspace:* @@ -1252,6 +1252,9 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) + '@tanstack/react-query': + specifier: 5.45.1 + version: 5.45.1(react@18.3.1) '@tanstack/react-table': specifier: ^8.17.3 version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10866,7 +10869,7 @@ snapshots: '@sentry/types': 8.11.0 '@sentry/utils': 8.11.0 - '@sentry/nextjs@8.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1)': + '@sentry/nextjs@8.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)(next@14.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.1)': dependencies: '@opentelemetry/instrumentation-http': 0.52.0(@opentelemetry/api@1.9.0) '@rollup/plugin-commonjs': 26.0.1(rollup@3.29.4) diff --git a/turbo/generators/templates/env/generator.ts b/turbo/generators/templates/env/generator.ts index 8c6af3b64..49a8e5fab 100644 --- a/turbo/generators/templates/env/generator.ts +++ b/turbo/generators/templates/env/generator.ts @@ -3,7 +3,8 @@ import { writeFileSync } from 'node:fs'; import { generator } from '../../utils'; -const DOCS_URL = 'https://makerkit.dev/docs/next-supabase-turbo/environment-variables'; +const DOCS_URL = + 'https://makerkit.dev/docs/next-supabase-turbo/environment-variables'; export function createEnvironmentVariablesGenerator( plop: PlopTypes.NodePlopAPI, @@ -41,8 +42,7 @@ export function createEnvironmentVariablesGenerator( { type: 'input', name: 'values.NEXT_PUBLIC_SITE_URL', - message: - `What is the site URL of you website? (Ex. https://makerkit.dev). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_URL')}\n`, + message: `What is the site URL of you website? (Ex. https://makerkit.dev). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_URL')}\n`, default: allVariables.NEXT_PUBLIC_SITE_URL, }, { @@ -54,58 +54,56 @@ export function createEnvironmentVariablesGenerator( { type: 'input', name: 'values.NEXT_PUBLIC_SITE_TITLE', - message: - `What is the title of your website? (Ex. MakerKit - The best way to make things). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_TITLE')}\n`, + message: `What is the title of your website? (Ex. MakerKit - The best way to make things). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_TITLE')}\n`, default: allVariables.NEXT_PUBLIC_SITE_TITLE, }, { type: 'input', name: 'values.NEXT_PUBLIC_SITE_DESCRIPTION', - message: - `What is the description of your website? (Ex. MakerKit is the best way to make things and stuff). \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_SITE_DESCRIPTION")}\n`, + message: `What is the description of your website? (Ex. MakerKit is the best way to make things and stuff). \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SITE_DESCRIPTION')}\n`, default: allVariables.NEXT_PUBLIC_SITE_DESCRIPTION, }, { type: 'list', name: 'values.NEXT_PUBLIC_DEFAULT_THEME_MODE', - message: `What is the default theme mode of your website? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_DEFAULT_THEME_MODE")}\n`, + message: `What is the default theme mode of your website? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_DEFAULT_THEME_MODE')}\n`, choices: ['light', 'dark', 'system'], default: allVariables.NEXT_PUBLIC_DEFAULT_THEME_MODE ?? 'light', }, { type: 'input', name: 'values.NEXT_PUBLIC_DEFAULT_LOCALE', - message: `What is the default locale of your website? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_DEFAULT_LOCALE")}\n`, + message: `What is the default locale of your website? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_DEFAULT_LOCALE')}\n`, default: allVariables.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en', }, { type: 'confirm', name: 'values.NEXT_PUBLIC_AUTH_PASSWORD', - message: `Do you want to use email/password authentication? If not - we will hide the password login from the UI. \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_AUTH_PASSWORD")}\n`, + message: `Do you want to use email/password authentication? If not - we will hide the password login from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_AUTH_PASSWORD')}\n`, default: getBoolean(allVariables.NEXT_PUBLIC_AUTH_PASSWORD, true), }, { type: 'confirm', name: 'values.NEXT_PUBLIC_AUTH_MAGIC_LINK', - message: `Do you want to use magic link authentication? If not - we will hide the magic link login from the UI. \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_AUTH_MAGIC_LINK")}\n`, + message: `Do you want to use magic link authentication? If not - we will hide the magic link login from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_AUTH_MAGIC_LINK')}\n`, default: getBoolean(allVariables.NEXT_PUBLIC_AUTH_MAGIC_LINK, false), }, { type: 'input', name: 'values.CONTACT_EMAIL', - message: `What is the contact email you want to receive emails to? \nFor more info: ${getUrlToDocs("CONTACT_EMAIL")}\n`, + message: `What is the contact email you want to receive emails to? \nFor more info: ${getUrlToDocs('CONTACT_EMAIL')}\n`, default: allVariables.CONTACT_EMAIL, }, { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_THEME_TOGGLE', - message: `Do you want to enable the theme toggle? If not - we will hide the theme toggle from the UI. \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_THEME_TOGGLE")}\n`, + message: `Do you want to enable the theme toggle? If not - we will hide the theme toggle from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_THEME_TOGGLE')}\n`, default: getBoolean(allVariables.NEXT_PUBLIC_ENABLE_THEME_TOGGLE, true), }, { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION', - message: `Do you want to enable personal account deletion? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION")}\n`, + message: `Do you want to enable personal account deletion? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION, true, @@ -114,7 +112,7 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING', - message: `Do you want to enable personal account billing? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING")}\n`, + message: `Do you want to enable personal account billing? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING, true, @@ -123,7 +121,7 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS', - message: `Do you want to enable team accounts? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS")}\n`, + message: `Do you want to enable team accounts? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS, true, @@ -132,7 +130,7 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNT_DELETION', - message: `Do you want to enable team account deletion? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_TEAM_ACCOUNT_DELETION")}\n`, + message: `Do you want to enable team account deletion? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNT_DELETION')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNT_DELETION, true, @@ -141,9 +139,8 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING', - message: `Do you want to enable team account billing? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING")}\n`, + message: `Do you want to enable team account billing? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING')}\n`, default: getBoolean( - allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING, true, ), @@ -151,7 +148,7 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION', - message: `Do you want to enable team account creation? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION")}\n`, + message: `Do you want to enable team account creation? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION, true, @@ -160,8 +157,7 @@ export function createEnvironmentVariablesGenerator( { type: 'confirm', name: 'values.NEXT_PUBLIC_ENABLE_NOTIFICATIONS', - message: - `Do you want to enable notifications? If not - we will hide the notifications bell from the UI. \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_ENABLE_NOTIFICATIONS")}\n`, + message: `Do you want to enable notifications? If not - we will hide the notifications bell from the UI. \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_NOTIFICATIONS')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_ENABLE_NOTIFICATIONS, true, @@ -171,33 +167,41 @@ export function createEnvironmentVariablesGenerator( when: (answers) => answers.values.NEXT_PUBLIC_ENABLE_NOTIFICATIONS, type: 'confirm', name: 'values.NEXT_PUBLIC_REALTIME_NOTIFICATIONS', - message: - `Do you want to enable realtime notifications? If yes, we will enable the realtime notifications from Supabase. If not - updated will be fetched lazily.\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_REALTIME_NOTIFICATIONS")}\n`, + message: `Do you want to enable realtime notifications? If yes, we will enable the realtime notifications from Supabase. If not - updated will be fetched lazily.\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_REALTIME_NOTIFICATIONS')}\n`, default: getBoolean( allVariables.NEXT_PUBLIC_REALTIME_NOTIFICATIONS, false, ), }, + { + type: 'input', + name: 'values.NEXT_PUBLIC_ENABLE_VERSION_UPDATER', + message: `Do you want to enable the version updater popup? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_ENABLE_VERSION_UPDATER')}\n`, + default: getBoolean( + allVariables.NEXT_PUBLIC_ENABLE_VERSION_UPDATER, + false, + ), + }, { type: 'input', name: 'values.NEXT_PUBLIC_SUPABASE_URL', - message: `What is the Supabase URL? (Ex. https://yourapp.supabase.co).\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_SUPABASE_URL")}\n`, + message: `What is the Supabase URL? (Ex. https://yourapp.supabase.co).\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SUPABASE_URL')}\n`, default: allVariables.NEXT_PUBLIC_SUPABASE_URL, }, { type: 'input', name: 'values.NEXT_PUBLIC_SUPABASE_ANON_KEY', - message: `What is the Supabase anon key?\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_SUPABASE_ANON_KEY")}\n`, + message: `What is the Supabase anon key?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_SUPABASE_ANON_KEY')}\n`, }, { type: 'input', name: 'values.SUPABASE_SERVICE_ROLE_KEY', - message: `What is the Supabase Service Role Key?\nFor more info: ${getUrlToDocs("SUPABASE_SERVICE_ROLE_KEY")}\n`, + message: `What is the Supabase Service Role Key?\nFor more info: ${getUrlToDocs('SUPABASE_SERVICE_ROLE_KEY')}\n`, }, { type: 'list', name: 'values.NEXT_PUBLIC_BILLING_PROVIDER', - message: `What is the billing provider you want to use?\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_BILLING_PROVIDER")}\n`, + message: `What is the billing provider you want to use?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_BILLING_PROVIDER')}\n`, choices: ['stripe', 'lemon-squeezy'], default: allVariables.NEXT_PUBLIC_BILLING_PROVIDER ?? 'stripe', }, @@ -206,7 +210,7 @@ export function createEnvironmentVariablesGenerator( answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe', type: 'input', name: 'values.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', - message: `What is the Stripe publishable key?\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY")}\n`, + message: `What is the Stripe publishable key?\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY')}\n`, default: allVariables.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, }, { @@ -214,28 +218,28 @@ export function createEnvironmentVariablesGenerator( answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe', type: 'input', name: 'values.STRIPE_SECRET_KEY', - message: `What is the Stripe secret key? \nFor more info: ${getUrlToDocs("NEXT_PUBLIC_BILLING_PROVIDER")}\n`, + message: `What is the Stripe secret key? \nFor more info: ${getUrlToDocs('NEXT_PUBLIC_BILLING_PROVIDER')}\n`, }, { when: (answers) => answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'stripe', type: 'input', name: 'values.STRIPE_WEBHOOK_SECRET', - message: `What is the Stripe webhook secret? \nFor more info: ${getUrlToDocs("STRIPE_WEBHOOK_SECRET")}\n`, + message: `What is the Stripe webhook secret? \nFor more info: ${getUrlToDocs('STRIPE_WEBHOOK_SECRET')}\n`, }, { when: (answers) => answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy', type: 'input', name: 'values.LEMON_SQUEEZY_SECRET_KEY', - message: `What is the Lemon Squeezy secret key? \nFor more info: ${getUrlToDocs("LEMON_SQUEEZY_SECRET_KEY")}\n`, + message: `What is the Lemon Squeezy secret key? \nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_SECRET_KEY')}\n`, }, { when: (answers) => answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy', type: 'input', - name: 'values.LEMON_SQUEEZY_STORE_ID', - message: `What is the Lemon Squeezy store ID? \nFor more info: ${getUrlToDocs("LEMON_SQUEEZY_STORE_ID")}\n`, + name: 'values.LEMON_SQUEEZY_STORE_ID', + message: `What is the Lemon Squeezy store ID? \nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_STORE_ID')}\n`, default: allVariables.LEMON_SQUEEZY_STORE_ID, }, { @@ -243,24 +247,24 @@ export function createEnvironmentVariablesGenerator( answers.values.NEXT_PUBLIC_BILLING_PROVIDER === 'lemon-squeezy', type: 'input', name: 'values.LEMON_SQUEEZY_SIGNING_SECRET', - message: `What is the Lemon Squeezy signing secret?\nFor more info: ${getUrlToDocs("LEMON_SQUEEZY_SIGNING_SECRET")}\n`, + message: `What is the Lemon Squeezy signing secret?\nFor more info: ${getUrlToDocs('LEMON_SQUEEZY_SIGNING_SECRET')}\n`, }, { type: 'input', name: 'values.SUPABASE_DB_WEBHOOK_SECRET', - message: `What is the DB webhook secret?\nFor more info: ${getUrlToDocs("SUPABASE_DB_WEBHOOK_SECRET")}\n`, + message: `What is the DB webhook secret?\nFor more info: ${getUrlToDocs('SUPABASE_DB_WEBHOOK_SECRET')}\n`, }, { type: 'list', name: 'values.CMS_CLIENT', - message: `What is the CMS client you want to use?\nFor more info: ${getUrlToDocs("CMS_CLIENT")}\n`, + message: `What is the CMS client you want to use?\nFor more info: ${getUrlToDocs('CMS_CLIENT')}\n`, choices: ['keystatic', 'wordpress'], default: allVariables.CMS_CLIENT ?? 'keystatic', }, { type: 'list', name: 'values.MAILER_PROVIDER', - message: `What is the mailer provider you want to use?\nFor more info: ${getUrlToDocs("MAILER_PROVIDER")}\n`, + message: `What is the mailer provider you want to use?\nFor more info: ${getUrlToDocs('MAILER_PROVIDER')}\n`, choices: ['nodemailer', 'resend'], default: allVariables.MAILER_PROVIDER ?? 'nodemailer', }, @@ -268,64 +272,61 @@ export function createEnvironmentVariablesGenerator( when: (answers) => answers.values.MAILER_PROVIDER === 'resend', type: 'input', name: 'values.RESEND_API_KEY', - message: `What is the Resend API key?\nFor more info: ${getUrlToDocs("RESEND_API_KEY")}\n`, + message: `What is the Resend API key?\nFor more info: ${getUrlToDocs('RESEND_API_KEY')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'input', name: 'values.EMAIL_SENDER', - message: `What is the email sender? (ex. info@makerkit.dev).\nFor more info: ${getUrlToDocs("EMAIL_SENDER")}\n`, + message: `What is the email sender? (ex. info@makerkit.dev).\nFor more info: ${getUrlToDocs('EMAIL_SENDER')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'input', name: 'values.EMAIL_HOST', - message: `What is the email host?\nFor more info: ${getUrlToDocs("EMAIL_HOST")}\n`, + message: `What is the email host?\nFor more info: ${getUrlToDocs('EMAIL_HOST')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'input', name: 'values.EMAIL_PORT', - message: `What is the email port?\nFor more info: ${getUrlToDocs("EMAIL_PORT")}\n`, + message: `What is the email port?\nFor more info: ${getUrlToDocs('EMAIL_PORT')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'input', name: 'values.EMAIL_USER', - message: `What is the email username? (check your email provider).\nFor more info: ${getUrlToDocs("EMAIL_USER")}\n`, + message: `What is the email username? (check your email provider).\nFor more info: ${getUrlToDocs('EMAIL_USER')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'input', name: 'values.EMAIL_PASSWORD', - message: `What is the email password? (check your email provider).\nFor more info: ${getUrlToDocs("EMAIL_PASSWORD")}\n`, + message: `What is the email password? (check your email provider).\nFor more info: ${getUrlToDocs('EMAIL_PASSWORD')}\n`, }, { when: (answers) => answers.values.MAILER_PROVIDER === 'nodemailer', type: 'confirm', name: 'values.EMAIL_TLS', - message: `Do you want to enable TLS for your emails?\nFor more info: ${getUrlToDocs("EMAIL_TLS")}\n`, + message: `Do you want to enable TLS for your emails?\nFor more info: ${getUrlToDocs('EMAIL_TLS')}\n`, default: getBoolean(allVariables.EMAIL_TLS, true), }, { type: 'confirm', name: 'captcha', - message: - `Do you want to enable Cloudflare Captcha protection for the Auth endpoints?`, + message: `Do you want to enable Cloudflare Captcha protection for the Auth endpoints?`, }, { when: (answers) => answers.captcha, type: 'input', name: 'values.NEXT_PUBLIC_CAPTCHA_SITE_KEY', - message: - `What is the Cloudflare Captcha site key? NB: this is the PUBLIC key!\nFor more info: ${getUrlToDocs("NEXT_PUBLIC_CAPTCHA_SITE_KEY")}\n`, + message: `What is the Cloudflare Captcha site key? NB: this is the PUBLIC key!\nFor more info: ${getUrlToDocs('NEXT_PUBLIC_CAPTCHA_SITE_KEY')}\n`, }, { when: (answers) => answers.captcha, type: 'input', name: 'values.CAPTCHA_SECRET_TOKEN', - message: - `What is the Cloudflare Captcha secret key? NB: this is the PRIVATE key!\nFor more info: ${getUrlToDocs("CAPTCHA_SECRET_TOKEN")}\n`, + message: `What is the Cloudflare Captcha secret key? NB: this is the PRIVATE key!\nFor more info: ${getUrlToDocs('CAPTCHA_SECRET_TOKEN')}\n`, }, ], }); @@ -337,4 +338,4 @@ function getBoolean(value: string | undefined, defaultValue: boolean) { function getUrlToDocs(envVar: string) { return `${DOCS_URL}#${envVar.toLowerCase()}`; -} \ No newline at end of file +}