diff --git a/.changeset/fiery-pigs-kiss.md b/.changeset/fiery-pigs-kiss.md new file mode 100644 index 00000000000..931d9cc505b --- /dev/null +++ b/.changeset/fiery-pigs-kiss.md @@ -0,0 +1,6 @@ +--- +"saleor-dashboard": patch +--- + +When App Store is not configured (env variable not set), Dashboard will not longer crash. Instead it will load +local catalog of apps and plugins to render them as a fallback diff --git a/.devcontainer/dashboard.env b/.devcontainer/dashboard.env index 5f0ff931383..1c3bdc25d97 100644 --- a/.devcontainer/dashboard.env +++ b/.devcontainer/dashboard.env @@ -8,9 +8,6 @@ API_URL=http://localhost:8000/graphql/ APP_MOUNT_URI=/dashboard/ STATIC_URL=/dashboard/ -# Marketplace API URL to get Saleor app listings, used to retrieve information about -# apps from the Saleor App Marketplace. -APPS_MARKETPLACE_API_URL=https://apps.saleor.io/api/v2/saleor-apps EXTENSIONS_API_URL=https://apps.saleor.io/api/v1/extensions # Language for the Dashboard (ISO 639-1 format, e.g., `EN` for English, diff --git a/.env.template b/.env.template index bef5c51912a..4a0d746dd09 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,7 @@ API_URL=http://localhost:8000/graphql/ APP_MOUNT_URI=/ -APPS_MARKETPLACE_API_URL=https://apps.saleor.io/api/v2/saleor-apps -EXTENSIONS_API_URL=https://apps.saleor.io/api/v1/extensions +# Provides source for App Store / explore feature. Only for Saleor Cloud +# EXTENSIONS_API_URL=https://apps.saleor.io/api/v1/extensions LOCALE_CODE="EN" # Multi-schema support (optional) diff --git a/.github/actions/run-pw-tests/action.yml b/.github/actions/run-pw-tests/action.yml index 4a23db59ac4..cc890e57da2 100644 --- a/.github/actions/run-pw-tests/action.yml +++ b/.github/actions/run-pw-tests/action.yml @@ -108,9 +108,6 @@ runs: - name: Run tests shell: bash env: - ## backward compatibility for older versions - API_URI: ${{ inputs.API_URL }} - API_URL: ${{ inputs.API_URL }} BASE_URL: ${{ inputs.BASE_URL }} E2E_USER_NAME: ${{ inputs.E2E_USER_NAME }} diff --git a/.github/workflows/deploy-cloud.yaml b/.github/workflows/deploy-cloud.yaml index 66730fb8edc..403f2507dbf 100644 --- a/.github/workflows/deploy-cloud.yaml +++ b/.github/workflows/deploy-cloud.yaml @@ -33,11 +33,6 @@ jobs: outputs: ENVIRONMENT: ${{ env.ENVIRONMENT }} env: - ## backward compatibility for older versions - APPS_MARKETPLACE_API_URI: "https://apps.saleor.io/api/v2/saleor-apps" - ## backward compatibility for older versions - API_URI: /graphql/ - API_URL: /graphql/ APP_MOUNT_URI: /dashboard/ STATIC_URL: /dashboard/static/ @@ -48,7 +43,6 @@ jobs: REGION: ${{ github.event.client_payload.region }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - APPS_MARKETPLACE_API_URL: "https://apps.saleor.io/api/v2/saleor-apps" EXTENSIONS_API_URL: "https://apps.saleor.io/api/v1/extensions" IS_CLOUD_INSTANCE: true POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 2005e183c79..797d9e1a175 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -15,11 +15,6 @@ jobs: id-token: write # needed by aws-actions/configure-aws-credentials contents: read env: - ## backward compatibility for older versions - APPS_MARKETPLACE_API_URI: "https://apps.staging.saleor.io/api/v2/saleor-apps" - ## backward compatibility for older versions - API_URI: /graphql/ - API_URL: /graphql/ APP_MOUNT_URI: /dashboard/ STATIC_URL: /dashboard/static/ @@ -28,7 +23,6 @@ jobs: SENTRY_URL_PREFIX: "~/dashboard/static" SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - APPS_MARKETPLACE_API_URL: "https://apps.staging.saleor.io/api/v2/saleor-apps" EXTENSIONS_API_URL: "https://apps.staging.saleor.io/api/v1/extensions" ENABLED_SERVICE_NAME_HEADER: true ONBOARDING_USER_JOINED_DATE_THRESHOLD: ${{ vars.DEV_ONBOARDING_USER_JOINED_DATE_THRESHOLD }} diff --git a/.github/workflows/deploy-master-staging.yaml b/.github/workflows/deploy-master-staging.yaml index a9990b24bba..2b7eeb273a9 100644 --- a/.github/workflows/deploy-master-staging.yaml +++ b/.github/workflows/deploy-master-staging.yaml @@ -14,11 +14,6 @@ jobs: outputs: CUSTOM_VERSION: ${{ env.CUSTOM_VERSION }} env: - ## backward compatibility for older versions - API_URI: /graphql/ - ## backward compatibility for older versions - APPS_MARKETPLACE_API_URI: "https://apps.staging.saleor.io/api/v2/saleor-apps" - API_URL: /graphql/ APP_MOUNT_URI: /dashboard/ STATIC_URL: /dashboard/static/ @@ -27,7 +22,6 @@ jobs: SENTRY_URL_PREFIX: "~/dashboard/static" SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - APPS_MARKETPLACE_API_URL: "https://apps.staging.saleor.io/api/v2/saleor-apps" EXTENSIONS_API_URL: "https://apps.staging.saleor.io/api/v1/extensions" IS_CLOUD_INSTANCE: true ENABLED_SERVICE_NAME_HEADER: true diff --git a/.github/workflows/deploy-staging-and-prepare-release.yaml b/.github/workflows/deploy-staging-and-prepare-release.yaml index 306d1b369fa..b22d3d8b88d 100644 --- a/.github/workflows/deploy-staging-and-prepare-release.yaml +++ b/.github/workflows/deploy-staging-and-prepare-release.yaml @@ -65,11 +65,6 @@ jobs: CUSTOM_VERSION: ${{ env.CUSTOM_VERSION || env.VERSION }} ENVIRONMENT: ${{ env.ENVIRONMENT }} env: - ## backward compatibility for older versions - APPS_MARKETPLACE_API_URI: "https://apps.staging.saleor.io/api/v2/saleor-apps" - ## backward compatibility for older versions - API_URI: /graphql/ - API_URL: /graphql/ APP_MOUNT_URI: /dashboard/ STATIC_URL: /dashboard/static/ @@ -78,7 +73,6 @@ jobs: SENTRY_URL_PREFIX: "~/dashboard/static" SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - APPS_MARKETPLACE_API_URL: "https://apps.staging.saleor.io/api/v2/saleor-apps" EXTENSIONS_API_URL: "https://apps.staging.saleor.io/api/v1/extensions" VERSION: ${{ github.event.inputs.git_ref || github.ref_name }} IS_CLOUD_INSTANCE: true diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index 7fd79b0a462..25e85ebbf71 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -120,13 +120,7 @@ jobs: - name: Build dashboard id: build-dashboard env: - ## backward compatibility for older versions - API_URI: ${{ needs.initialize-cloud.outputs.API_URL }} - ## backward compatibility for older versions - APPS_MARKETPLACE_API_URI: "https://apps.staging.saleor.io/api/v2/saleor-apps" - API_URL: ${{ needs.initialize-cloud.outputs.API_URL }} - APPS_MARKETPLACE_API_URL: "https://apps.staging.saleor.io/api/v2/saleor-apps" EXTENSIONS_API_URL: "https://apps.staging.saleor.io/api/v1/extensions" APP_MOUNT_URI: / STATIC_URL: / diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 20cea452382..df85c1b5c33 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -3,9 +3,7 @@ API_URL: "/graphql/", APP_MOUNT_URI: "/", STATIC_URL: "/", - APPS_MARKETPLACE_API_URL: "https://marketplace.saleor.io", EXTENSIONS_API_URL: "https://extensions.saleor.io", - APPS_TUNNEL_URL_KEYWORDS: "", IS_CLOUD_INSTANCE: "false", LOCALE_CODE: "", }; diff --git a/Dockerfile b/Dockerfile index e49d6269feb..eb41412f3e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,18 +22,14 @@ COPY src/ src/ ARG API_URL ARG APP_MOUNT_URI -ARG APPS_MARKETPLACE_API_URL ARG EXTENSIONS_API_URL -ARG APPS_TUNNEL_URL_KEYWORDS ARG STATIC_URL ARG SKIP_SOURCEMAPS ARG LOCALE_CODE ENV API_URL="${API_URL:-http://localhost:8000/graphql/}" ENV APP_MOUNT_URI="${APP_MOUNT_URI:-/dashboard/}" -ENV APPS_MARKETPLACE_API_URL="${APPS_MARKETPLACE_API_URL:-https://apps.saleor.io/api/v2/saleor-apps}" -ENV EXTENSIONS_API_URL="${EXTENSIONS_API_URL:-https://apps.saleor.io/api/v1/extensions}" -ENV APPS_TUNNEL_URL_KEYWORDS="${APPS_TUNNEL_URL_KEYWORDS}" +ENV EXTENSIONS_API_URL="${EXTENSIONS_API_URL}" ENV STATIC_URL="${STATIC_URL:-/dashboard/}" ENV SKIP_SOURCEMAPS="${SKIP_SOURCEMAPS:-true}" ENV LOCALE_CODE="${LOCALE_CODE:-EN}" diff --git a/docs/configuration.md b/docs/configuration.md index 645e0ebc399..915538e6856 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -18,11 +18,7 @@ Create or edit `.env` file in a root directory or set environment variables with - `STATIC_URL` - URL where the static files are located. E.g., if you use an S3 bucket, you should set it to the bucket's URL. By default, Saleor assumes you serve static files from the root of your site at "http://localhost:9000/". -- `APPS_MARKETPLACE_API_URL` - URI of Marketplace API to fetch list of Apps in JSON. - -- `EXTENSIONS_API_URL` - URI of Marketplace API to fetch list of Extensions in JSON. - -- `APPS_TUNNEL_URL_KEYWORDS` - Custom apps tunnel URL keywords. +- `EXTENSIONS_API_URL` - Optional URI of the Saleor Marketplace API used to fetch the list of extensions in JSON. Saleor Cloud projects have this preconfigured; self-hosted deployments can omit it and the Dashboard will fall back to a bundled `extensions.json` dataset and show a self-hosted banner in the Explore view. ## Fetching schema diff --git a/docs/docker.md b/docs/docker.md index 5d75bfc0de0..51908913b23 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -26,9 +26,7 @@ The replacement is not limited to `API_URL` only. You can also replace other env docker exec -it \ -e "API_URL=NEW_API_URL" \ -e "APP_MOUNT_URI=NEW_APP_MOUNT_URI" \ - -e "APPS_MARKETPLACE_API_URL=NEW_APPS_MARKETPLACE_API_URL" \ -e "EXTENSIONS_API_URL=NEW_EXTENSIONS_API_URL" \ - -e "APPS_TUNNEL_URL_KEYWORDS=NEW_APPS_TUNNEL_URL_KEYWORDS" \ -e "IS_CLOUD_INSTANCE=NEW_IS_CLOUD_INSTANCE" \ -e "LOCALE_CODE=NEW_LOCALE_CODE" \ saleor-dashboard /docker-entrypoint.d/50-replace-env-vars.sh @@ -40,9 +38,7 @@ Of course you can also provide all the environment variables at the `docker run` docker run --publish 8080:80 \ -e "API_URL=NEW_API_URL" \ -e "APP_MOUNT_URI=NEW_APP_MOUNT_URI" \ - -e "APPS_MARKETPLACE_API_URL=NEW_APPS_MARKETPLACE_API_URL" \ -e "EXTENSIONS_API_URL=NEW_EXTENSIONS_API_URL" \ - -e "APPS_TUNNEL_URL_KEYWORDS=NEW_APPS_TUNNEL_URL_KEYWORDS" \ -e "IS_CLOUD_INSTANCE=NEW_IS_CLOUD_INSTANCE" \ -e "LOCALE_CODE=NEW_LOCALE_CODE" \ saleor-dashboard diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 76da86bcb28..4bf7e741f0d 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1408,6 +1408,9 @@ "context": "tax strategy combobox hint", "string": "Select the method of tax calculation" }, + "6IteaF": { + "string": "Apps are available for Saleor Cloud users. Most of them are available for self-hosting." + }, "6J1m2c": { "string": "Create a Model" }, @@ -7784,9 +7787,6 @@ "gVqSnA": { "string": "Assign and save" }, - "gZ1qnD": { - "string": "No extensions API URL provided" - }, "gaOXvo": { "context": "notification, form submitted", "string": "Refund grant for order #{orderNumber} was updated" diff --git a/nginx/replace-env-vars.sh b/nginx/replace-env-vars.sh index 05ce8f24c99..eb00b1318ee 100755 --- a/nginx/replace-env-vars.sh +++ b/nginx/replace-env-vars.sh @@ -13,7 +13,7 @@ replace_env_var() { var_value=$(eval echo \$"$var_name") if [ -n "$var_value" ]; then echo "Setting $var_name to: $var_value" - sed -i "s#$var_name: \".*\"#$var_name: \"$var_value\"#" "$INDEX_BUNDLE_PATH" + sed -i "s#\([[:space:]]*\)$var_name:[[:space:]]*\"[^\"]*\"#\1$var_name: \"$var_value\"#" "$INDEX_BUNDLE_PATH" else echo "No $var_name provided, using defaults." fi @@ -22,9 +22,7 @@ replace_env_var() { # Replace each environment variable replace_env_var "API_URL" replace_env_var "APP_MOUNT_URI" -replace_env_var "APPS_MARKETPLACE_API_URL" replace_env_var "EXTENSIONS_API_URL" -replace_env_var "APPS_TUNNEL_URL_KEYWORDS" replace_env_var "IS_CLOUD_INSTANCE" replace_env_var "LOCALE_CODE" diff --git a/src/components/NavigatorSearch/useActionTriggers.tsx b/src/components/NavigatorSearch/useActionTriggers.tsx index b4d45c92b98..0ad251a29fa 100644 --- a/src/components/NavigatorSearch/useActionTriggers.tsx +++ b/src/components/NavigatorSearch/useActionTriggers.tsx @@ -600,7 +600,7 @@ const allActions: TriggerDescriptor[] = [ { section: allMessages.extensionsSection, name: allMessages.gotoExploreExtensions, - Component: ({ onClick }) => ( + Component: ({ onClick }: { onClick?: React.MouseEventHandler }) => ( diff --git a/src/components/Sidebar/menu/hooks/useMenuStructure.tsx b/src/components/Sidebar/menu/hooks/useMenuStructure.tsx index 532d5da8c46..9c4fde1e2d5 100644 --- a/src/components/Sidebar/menu/hooks/useMenuStructure.tsx +++ b/src/components/Sidebar/menu/hooks/useMenuStructure.tsx @@ -92,7 +92,7 @@ export function useMenuStructure() { id: "explore-extensions", url: ExtensionsPaths.exploreExtensions, permissions: [], - type: "item", + type: "item" as const, }, ], }); diff --git a/src/config.ts b/src/config.ts index 541f0e599ab..d44b8721786 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,15 +32,6 @@ export const getAbsoluteApiUrl = () => new URL(getApiUrl(), window.location.orig export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL ?? "300", 10); export const IS_CLOUD_INSTANCE = window.__SALEOR_CONFIG__.IS_CLOUD_INSTANCE === "true"; -export const getAppsConfig = () => ({ - marketplaceApiUri: window.__SALEOR_CONFIG__.APPS_MARKETPLACE_API_URL, - tunnelUrlKeywords: window.__SALEOR_CONFIG__.APPS_TUNNEL_URL_KEYWORDS?.split(";") || [ - ".ngrok.io", - ".saleor.live", - ".trycloudflare.com", - ], -}); - export const getExtensionsConfig = () => ({ extensionsApiUri: window.__SALEOR_CONFIG__.EXTENSIONS_API_URL, }); diff --git a/src/extensions/data/extensions.json b/src/extensions/data/extensions.json new file mode 100644 index 00000000000..04f36a54963 --- /dev/null +++ b/src/extensions/data/extensions.json @@ -0,0 +1,638 @@ +{ + "extensionCategories": [ + { + "id": "agentic-commerce", + "name": { + "en": "Agentic Commerce" + }, + "extensions": [ + { + "id": "saleor.app.instant-checkout", + "name": { + "en": "Instant Checkout with ChatGPT" + }, + "description": { + "en": "Publish your products to ChatGPT following the Agentic Commerce Protocol." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/OpenAI-black-monoblossom.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/OpenAI-white-monoblossom.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": null + } + ] + }, + { + "id": "payments", + "name": { + "en": "Payments" + }, + "extensions": [ + { + "id": "app.saleor.adyen", + "name": { + "en": "Adyen" + }, + "description": { + "en": "Accept a wide range of payment methods and currencies by using the Adyen integration." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/adyen.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/adyen.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": null + }, + { + "id": "app.saleor.stripe-v2", + "name": { + "en": "Stripe" + }, + "description": { + "en": "Finalize orders successfully by offering a variety of payment methods through the integration of Saleor and Stripe." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/stripe.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/stripe.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/stripe" + }, + { + "id": "saleor.app.authorizenet", + "name": { + "en": "Authorize.net" + }, + "description": { + "en": "Connect Saleor with Authorize.net payments." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/authorizenet.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/authorize-net-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-authorize.net" + }, + { + "id": "saleor.app.juspay", + "name": { + "en": "Juspay" + }, + "description": { + "en": "Connect Saleor with a unified global payments solution to access multiple gateways and offer seamless transactions to your customers." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/juspay.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/community/juspay.svg" + } + }, + "type": "APP", + "kind": "PARTNER", + "manifestUrl": null, + "repositoryUrl": "https://github.com/juspay/hyperswitch-saleor-payment-app" + }, + { + "id": "saleor.app.klarna", + "name": { + "en": "Klarna" + }, + "description": { + "en": "Connect Saleor with Klarna, the leading BNPL provider." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/klarna.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/community/klarna.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-klarna" + }, + { + "id": "saleor.io.dummy-payment-app", + "name": { + "en": "Dummy Payment App" + }, + "description": { + "en": "Bare-bones app for testing Saleor's Trnsactions API without a real payment provider." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/dummy-payment-app.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/dummy-payment-app.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/dummy-payment-app" + }, + { + "id": "saleor.app.np-atobarai", + "name": { + "en": "NP Atobarai (NP後払い)" + }, + "description": { + "en": "Connect with leading Japanese payment and pick-up provider." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/saleor-apps/np-atobarai.png" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/saleor-apps/np-atobarai.png" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/np-atobarai" + }, + { + "id": "saleor.app.sequra", + "name": { + "en": "seQura" + }, + "description": { + "en": "Connect Saleor with seQura - a global payments solution that offers buy now, pay later and installment payment options." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/sequra.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/community/sequra.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-sequra" + } + ] + }, + { + "id": "taxes", + "name": { + "en": "Taxes" + }, + "extensions": [ + { + "id": "saleor.app.avatax", + "name": { + "en": "Avalara AvaTax" + }, + "description": { + "en": "Automate your tax management process by integrating Saleor with AvaTax, a leading cloud-based tax calculation software." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/avatax.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/avatax.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/avatax" + }, + { + "id": "saleor.app.taxjar", + "name": { + "en": "TaxJar" + }, + "description": { + "en": "Simplify tax calculations for your Saleor orders and checkouts by relying on TaxJar, a trusted tax calculation solution." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/taxjar.png" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/taxjar.png" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-taxjar" + } + ] + }, + { + "id": "cms", + "name": { + "en": "CMS" + }, + "extensions": [ + { + "id": "saleor.app.cms2", + "name": { + "en": "Strapi CMS" + }, + "description": { + "en": "Integrate Strapi CMS with Saleor to manage your products alongside your content." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/strapi.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/strapi.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/cms" + }, + { + "id": "saleor.app.cms2", + "name": { + "en": "Contentful CMS" + }, + "description": { + "en": "Synchronize Saleor with Contentful CMS to extend your product management capabilities." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/contentful.png" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/contentful.png" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/cms" + }, + { + "id": "saleor.app.cms2", + "name": { + "en": "DatoCMS" + }, + "description": { + "en": "Leverage the composable architecture by connecting your Saleor products to DatoCMS." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/datocms.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/datocms.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/cms" + }, + { + "id": "saleor.app.cms2", + "name": { + "en": "Builder.io" + }, + "description": { + "en": "Build e-commerce apps with the best-in-class drag & drop editor." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/builder.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/builder.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/cms" + }, + { + "id": "saleor.app.cms2", + "name": { + "en": "Payload CMS" + }, + "description": { + "en": "Sync your Saleor products with Payload CMS - a TypeScript-first headless CMS." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/payloadcms.png" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/payloadcms.png" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/cms" + } + ] + }, + { + "id": "automation", + "name": { + "en": "Automation" + }, + "extensions": [ + { + "id": "saleor.app.search", + "name": { + "en": "Algolia" + }, + "description": { + "en": "Enable fast and efficient product search by integrating Saleor with Algolia." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/algolia.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/algolia.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/search" + }, + { + "id": "saleor.app.klaviyo", + "name": { + "en": "Klaviyo" + }, + "description": { + "en": "Deliver a personalized purchase experience by leveraging integrating Saleor with Klaviyo." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/saleor-apps/app-klaviyo.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/app-klaviyo-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/klaviyo" + }, + { + "id": "saleor.app.product-feed", + "name": { + "en": "Google Merchant Center" + }, + "description": { + "en": "Enable your products to appear in Google's shopping results by connecting Saleor with Google Merchant Center." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/google.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/google.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/products-feed", + "manifestUrl": null + }, + { + "id": "saleor.app.segment-v2", + "name": { + "en": "Twilio Segment" + }, + "description": { + "en": "Reach your customers through video, voice, or email by leveraging the Saleor-Twilio Segment integration." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/segment.png" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/community/segment.png" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/segment" + }, + { + "id": "saleor.app.smtp", + "name": { + "en": "SMTP" + }, + "description": { + "en": "Take full control over your email communication by creating custom SMTP configurations within Saleor." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/smtp.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/app-smtp-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/apps/tree/HEAD/apps/smtp" + }, + { + "id": "saleor.app.invoices", + "name": { + "en": "Invoices" + }, + "description": { + "en": "Generate PDF invoices for Orders and store them in Saleor file storage." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/app-invoices.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/app-invoices-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-invoices" + }, + { + "id": "saleor.app.crm", + "name": { + "en": "CRM" + }, + "description": { + "en": "Providing customer data to external services like Mailchimp." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/app-crm.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/app-crm-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-crm" + }, + { + "id": "saleor.app.slack", + "name": { + "en": "Slack" + }, + "description": { + "en": "Never miss a beat by receiving real-time notifications on Slack whenever new orders come into your Saleor store." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v2/community/slack.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v2/community/slack.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-slack" + }, + { + "id": "saleor.app.sendgrid", + "name": { + "en": "Twilio SendGrid" + }, + "description": { + "en": "Connect Saleor to SendGrid." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v3/sendgrid.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/sendgrid.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-sendgrid" + }, + { + "id": "saleor.app.checkout-prices-poc", + "name": { + "en": "Checkout prices" + }, + "description": { + "en": "Set custom prices for checkout lines." + }, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v3/generic-app.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/generic-app-dark.svg" + } + }, + "type": "APP", + "kind": "OFFICIAL", + "manifestUrl": null, + "repositoryUrl": "https://github.com/saleor/examples/tree/HEAD/example-app-checkout-prices" + }, + { + "id": "mirumee.authentication.openidconnect", + "name": { + "en": "OpenID Connect (OIDC)" + }, + "description": null, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + } + }, + "type": "PLUGIN" + }, + { + "id": "mirumee.notifications.user_email", + "name": { + "en": "User Emails" + }, + "description": null, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + } + }, + "type": "PLUGIN" + }, + { + "id": "mirumee.notifications.admin_email", + "name": { + "en": "Admin Emails" + }, + "description": null, + "logo": { + "light": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + }, + "dark": { + "source": "https://apps.saleor.io/apps/v3/plugin.svg" + } + }, + "type": "PLUGIN" + } + ] + } + ] +} diff --git a/src/extensions/data/readme.md b/src/extensions/data/readme.md new file mode 100644 index 00000000000..f8feaf1a390 --- /dev/null +++ b/src/extensions/data/readme.md @@ -0,0 +1,5 @@ +extensions.json contain static list of plugins and apps to be displayed in "explore" page. + +This is fallback list for open source. For Saleor Cloud all extensions (apps and plugins) are listed via EXTENSIONS_API_URL endpoint + +This list should be synced with App Store in case of new apps are available there diff --git a/src/extensions/messages.ts b/src/extensions/messages.ts index 0753f2f5f70..7377d000b97 100644 --- a/src/extensions/messages.ts +++ b/src/extensions/messages.ts @@ -216,9 +216,10 @@ export const messages = defineMessages({ defaultMessage: "Learn more", id: "TdTXXf", }, - emptyExtensionsApiUrl: { - defaultMessage: "No extensions API URL provided", - id: "gZ1qnD", + selfHostedBanner: { + defaultMessage: + "Apps are available for Saleor Cloud users. Most of them are available for self-hosting.", + id: "6IteaF", }, noExtensionsInstalled: { defaultMessage: "No extensions installed yet", diff --git a/src/extensions/schema.ts b/src/extensions/schema.ts new file mode 100644 index 00000000000..a8e66a38980 --- /dev/null +++ b/src/extensions/schema.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +const localizedStringSchema = z.object({ + en: z.string(), +}); + +const logoSchema = z.object({ + light: z.object({ source: z.string() }), + dark: z.object({ source: z.string() }), +}); + +const commonExtensionFields = { + id: z.string(), + name: localizedStringSchema, + description: localizedStringSchema.nullable(), + logo: logoSchema, + installed: z.boolean().optional(), + disabled: z.boolean().optional(), +}; + +const appExtensionDataSchema = z.object({ + ...commonExtensionFields, + type: z.literal("APP"), + kind: z.enum(["OFFICIAL", "PARTNER", "OSS"]), + manifestUrl: z.string().nullable(), + repositoryUrl: z.string().nullable(), + isCustomApp: z.boolean().optional(), + appId: z.string().optional(), +}); + +const pluginExtensionDataSchema = z.object({ + ...commonExtensionFields, + type: z.literal("PLUGIN"), +}); + +export const extensionDataSchema = z.discriminatedUnion("type", [ + appExtensionDataSchema, + pluginExtensionDataSchema, +]); + +export const extensionCategorySchema = z.object({ + id: z.string(), + name: localizedStringSchema, + extensions: z.array(extensionDataSchema), +}); + +export const extensionsResponseSchema = z.object({ + extensionCategories: z.array(extensionCategorySchema), +}); + +export type ExtensionData = z.infer; +export type ExtensionCategory = z.infer; +export type ExtensionsResponse = z.infer; diff --git a/src/extensions/types.ts b/src/extensions/types.ts index 5054d081874..16568e7a7bc 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -1,5 +1,6 @@ import { type AllAppExtensionMounts } from "@dashboard/extensions/domain/app-extension-manifest-available-mounts"; import { type AppExtensionManifestTarget } from "@dashboard/extensions/domain/app-extension-manifest-target"; +import { type ExtensionCategory, type ExtensionData } from "@dashboard/extensions/schema"; import { type AppTypeEnum, type ExtensionListQuery, @@ -46,50 +47,11 @@ export const getProblemSortDate = (problem: AppProblem): string => { return problem.createdAt; }; -interface CommonExtensionData { - id: string; - name: { - en: string; - }; - description: { - en: string; - }; - logo: { - light: { - source: string; - }; - dark: { - source: string; - }; - }; - installed?: boolean; - disabled?: boolean; -} +export type { ExtensionData }; -interface AppExtensionData extends CommonExtensionData { - type: "APP"; - kind: "OFFICIAL" | "OSS"; - manifestUrl: string | null; - repositoryUrl: string | null; // Typo in the original code - isCustomApp?: boolean; - appId?: string; -} - -interface PluginExtensionData extends CommonExtensionData { - type: "PLUGIN"; -} +export type APIExtensionsResponse = ExtensionCategory[]; -export type ExtensionData = AppExtensionData | PluginExtensionData; - -type ExtensionGroup = "payments" | "taxes" | "cms" | "automation"; - -export type ExtensionsGroups = Record; - -export type APIExtensionsResponse = Array<{ - id: string; - name: { en: string }; - extensions: ExtensionData[]; -}>; +export type ExtensionsGroups = Record; /* Candidate for refactoring. InstalledExtension is only one case. diff --git a/src/extensions/views/ExploreExtensions/ExploreExtensions.tsx b/src/extensions/views/ExploreExtensions/ExploreExtensions.tsx index dea71977d44..9aa51637b02 100644 --- a/src/extensions/views/ExploreExtensions/ExploreExtensions.tsx +++ b/src/extensions/views/ExploreExtensions/ExploreExtensions.tsx @@ -4,7 +4,8 @@ import SearchInput from "@dashboard/components/AppLayout/ListFilters/components/ import { DashboardCard } from "@dashboard/components/Card"; import { ListPageLayout } from "@dashboard/components/Layouts"; import { Box, Text } from "@saleor/macaw-ui-next"; -import { useIntl } from "react-intl"; +import { Info } from "lucide-react"; +import { FormattedMessage, useIntl } from "react-intl"; import { headerTitles, messages } from "../../messages"; import { ExploreExtensionsActions } from "./components/ExploreExtensionsActions"; @@ -14,7 +15,7 @@ import { useExtensionsFilter } from "./hooks/useExtenstionsFilter"; export const ExploreExtensions = () => { const intl = useIntl(); - const { extensions, loading, error } = useExploreExtensions(); + const { extensions, loading, error, isFallback } = useExploreExtensions(); const subtitle = useContextualLink("extensions"); const { handleQueryChange, query, filteredExtensions } = useExtensionsFilter({ extensions }); @@ -40,6 +41,24 @@ export const ExploreExtensions = () => { + {isFallback && ( + + + + + + + + + )} ({ + extensionCategories: [], +})); + describe("Extensions / hooks / useAppStoreExtensions", () => { afterEach(() => { jest.clearAllMocks(); }); - it("should return an error message when appStoreUrl is not provided", () => { + it("should load fallback data when appStoreUrl is not provided", async () => { // Act - const { result } = renderHook(() => useAppStoreExtensions()); + const { result, waitForNextUpdate } = renderHook(() => useAppStoreExtensions()); // Assert - expect(result.current.error).toBe("No extensions API URL provided"); + expect(result.current.loading).toBe(true); + expect(result.current.isFallback).toBe(true); + + await waitForNextUpdate(); + expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ - payments: { title: "", items: [] }, - cms: { title: "", items: [] }, - taxes: { title: "", items: [] }, - automation: { title: "", items: [] }, - }); + expect(result.current.error).toBe(null); + expect(result.current.isFallback).toBe(true); }); it("should fetch and set data when appStoreUrl is provided", async () => { @@ -59,14 +90,21 @@ describe("Extensions / hooks / useAppStoreExtensions", () => { // Assert expect(result.current.loading).toBe(true); + expect(result.current.isFallback).toBe(false); await waitForNextUpdate(); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toEqual({ - payments: { title: "Payments", items: [{ id: "1", name: "Stripe" }] }, - cms: { title: "CMS", items: [{ id: "2", name: "WordPress" }] }, + payments: { + title: "Payments", + items: [expect.objectContaining({ id: "1" })], + }, + cms: { + title: "CMS", + items: [expect.objectContaining({ id: "2" })], + }, taxes: { title: "Taxes", items: [] }, automation: { title: "Automation", items: [] }, }); @@ -88,11 +126,6 @@ describe("Extensions / hooks / useAppStoreExtensions", () => { expect(result.current.loading).toBe(false); expect(result.current.error).toBe("Network Error"); - expect(result.current.data).toEqual({ - payments: { title: "", items: [] }, - cms: { title: "", items: [] }, - taxes: { title: "", items: [] }, - automation: { title: "", items: [] }, - }); + expect(result.current.data).toEqual({}); }); }); diff --git a/src/extensions/views/ExploreExtensions/hooks/useAppStoreExtensions.ts b/src/extensions/views/ExploreExtensions/hooks/useAppStoreExtensions.ts index f2d031892ce..5a252ca9f51 100644 --- a/src/extensions/views/ExploreExtensions/hooks/useAppStoreExtensions.ts +++ b/src/extensions/views/ExploreExtensions/hooks/useAppStoreExtensions.ts @@ -1,11 +1,10 @@ -import { messages } from "@dashboard/extensions/messages"; +import { extensionsResponseSchema } from "@dashboard/extensions/schema"; import { type APIExtensionsResponse, type ExtensionsGroups } from "@dashboard/extensions/types"; import { useEffect, useState } from "react"; -import { useIntl } from "react-intl"; -const prepareExtensionsData = (data: APIExtensionsResponse) => { - return data.reduce((acc, { name, extensions }) => { - const group = name.en.toLowerCase() as keyof ExtensionsGroups; +const prepareExtensionsData = (data: APIExtensionsResponse): ExtensionsGroups => { + return data.reduce((acc, { name, extensions }) => { + const group = name.en.toLowerCase(); acc[group] = { title: name.en, @@ -13,46 +12,45 @@ const prepareExtensionsData = (data: APIExtensionsResponse) => { }; return acc; - }, {} as ExtensionsGroups); + }, {}); }; -export const useAppStoreExtensions = (appStoreUrl?: string) => { - const intl = useIntl(); +const loadFallbackExtensions = async (): Promise => { + const data = await import("@dashboard/extensions/data/extensions.json"); + const parsed = extensionsResponseSchema.parse(data.default ?? data); + + return parsed.extensionCategories; +}; + +const fetchApiExtensions = async (url: string): Promise => { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + const parsed = extensionsResponseSchema.parse(data); + + return parsed.extensionCategories; +}; +export const useAppStoreExtensions = (appStoreUrl?: string) => { const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const [data, setData] = useState({ - payments: { - items: [], - title: "", - }, - cms: { items: [], title: "" }, - taxes: { - items: [], - title: "", - }, - automation: { - items: [], - title: "", - }, - }); + const [loading, setLoading] = useState(true); + const [data, setData] = useState({}); + const isFallback = !appStoreUrl; useEffect(() => { const fetchData = async () => { - if (!appStoreUrl) return; - try { setLoading(true); - const response = await fetch(appStoreUrl); - - if (!response.ok) { - throw new Error(response.statusText); - } + const categories = appStoreUrl + ? await fetchApiExtensions(appStoreUrl) + : await loadFallbackExtensions(); - const data = await response.json(); - - setData(prepareExtensionsData(data.extensionCategories)); + setData(prepareExtensionsData(categories)); } catch (e) { setError((e as Error).message); } finally { @@ -63,17 +61,10 @@ export const useAppStoreExtensions = (appStoreUrl?: string) => { fetchData(); }, [appStoreUrl]); - if (!appStoreUrl) { - return { - error: intl.formatMessage(messages.emptyExtensionsApiUrl), - loading: false, - data, - }; - } - return { error, loading, data, + isFallback, }; }; diff --git a/src/extensions/views/ExploreExtensions/hooks/useExploreExtensions.ts b/src/extensions/views/ExploreExtensions/hooks/useExploreExtensions.ts index 4d8b706eae1..278e1c6b780 100644 --- a/src/extensions/views/ExploreExtensions/hooks/useExploreExtensions.ts +++ b/src/extensions/views/ExploreExtensions/hooks/useExploreExtensions.ts @@ -72,7 +72,9 @@ const getFilteredExtensions = ({ }; export const useExploreExtensions = () => { - const { data, loading, error } = useAppStoreExtensions(getExtensionsConfig().extensionsApiUri); + const { data, loading, error, isFallback } = useAppStoreExtensions( + getExtensionsConfig().extensionsApiUri, + ); const { data: installedAppsData } = useInstalledAppsQuery({ variables: { first: 100, @@ -112,5 +114,6 @@ export const useExploreExtensions = () => { extensions: extensionsData, loading, error, + isFallback, }; }; diff --git a/src/index.html b/src/index.html index feb4dc7fef8..d6c67f9dc09 100644 --- a/src/index.html +++ b/src/index.html @@ -16,9 +16,7 @@ API_URL: "<%= API_URL %>", APP_MOUNT_URI: "<%= APP_MOUNT_URI %>", STATIC_URL: "<%= STATIC_URL %>", - APPS_MARKETPLACE_API_URL: "<%= APPS_MARKETPLACE_API_URL %>", EXTENSIONS_API_URL: "<%= EXTENSIONS_API_URL %>", - APPS_TUNNEL_URL_KEYWORDS: "<%= APPS_TUNNEL_URL_KEYWORDS %>", IS_CLOUD_INSTANCE: "<%= IS_CLOUD_INSTANCE %>", LOCALE_CODE: "<%= LOCALE_CODE %>", }; diff --git a/testUtils/setup.ts b/testUtils/setup.ts index b69b0feaae2..def2a145d30 100644 --- a/testUtils/setup.ts +++ b/testUtils/setup.ts @@ -34,9 +34,7 @@ window.__SALEOR_CONFIG__ = { API_URL: "http://localhost:8000/graphql/", APP_MOUNT_URI: "/", STATIC_URL: "/", - APPS_MARKETPLACE_API_URL: "http://localhost:3000", EXTENSIONS_API_URL: "http://localhost:3000", - APPS_TUNNEL_URL_KEYWORDS: ".ngrok.io;.saleor.live", IS_CLOUD_INSTANCE: "true", LOCALE_CODE: "EN", }; diff --git a/types.d.ts b/types.d.ts index 64f15bf7e3b..fbb249f66c2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -21,9 +21,7 @@ declare interface Window { APP_MOUNT_URI: string; STATIC_URL?: string; LOCALE_CODE?: string; - APPS_MARKETPLACE_API_URL?: string; EXTENSIONS_API_URL?: string; - APPS_TUNNEL_URL_KEYWORDS?: string; IS_CLOUD_INSTANCE?: string; }; } diff --git a/vite.config.js b/vite.config.js index a72baabc03e..4a1bf27e0c5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -43,9 +43,7 @@ export default defineConfig(({ command, mode }) => { SENTRY_RELEASE, ENVIRONMENT, STATIC_URL, - APPS_MARKETPLACE_API_URL, EXTENSIONS_API_URL, - APPS_TUNNEL_URL_KEYWORDS, SKIP_SOURCEMAPS, CUSTOM_VERSION, FLAGS_SERVICE_ENABLED, @@ -84,9 +82,7 @@ export default defineConfig(({ command, mode }) => { API_URL, APP_MOUNT_URI, STATIC_URL, - APPS_MARKETPLACE_API_URL, - EXTENSIONS_API_URL, - APPS_TUNNEL_URL_KEYWORDS, + EXTENSIONS_API_URL: EXTENSIONS_API_URL ?? "", IS_CLOUD_INSTANCE, LOCALE_CODE, POSTHOG_KEY,