Files
2bb3207c34 feat(web): support chat with Pylon (#7536)
* feat(web): support chat with Pylon (#7459)

* feat(web): integrate external support-chat package

- Add @safe-global/safe-react-support-chat dependency
- Configure CSP headers for support chat iframe
- Integrate support chat widget in Header component
- Add support chat constants and configuration
- Create useSupportChat hook for managing chat state

* feat(web): add HelpMenu FAB with support chat integration

- Add HelpMenu component with FAB that transforms to X when chat open
- Integrate SupportChatDrawer from support-chat-embed package
- Update useSupportChat hook with local type definitions
- Add HelpMenu to PageLayout

* fix: Set requester name to Safe{Wallet} instead of wallet label

Ensures Pylon shows the source app rather than the wallet provider
name (e.g. "Rabby") as the support requester identity.

* refactor: remove support chat changes from Header

Keep Header unchanged from upstream - support chat is only accessed via the HelpMenu FAB.

* fix: remove unused shortenAddress function

* fix(web): update support chat config and remove What's new menu item

- Use link: instead of portal: for support-chat-embed to fix dependency hoisting
- Update production chat URL to safe-support.vercel.app
- Update allowed parents with correct production and localhost origins
- Remove unused What's new menu item from HelpMenu

* chore: add .npmrc for GitHub Packages registry auth

Configures @safe-global scope to resolve from GitHub Packages registry,
needed for the private @safe-global/support-chat-embed package.

Note: Currently using link: for local dev. To switch to published package
(1.0.0), the embed package needs to update its @mui/material peer dep
to "^5.0.0 || ^6.0.0" to avoid hoisting conflicts with MUI v6.

* fix(web): use published support-chat-embed and fix monorepo module resolution

Switch from local link to published @safe-global/support-chat-embed@^1.0.2
from GitHub Packages registry. Add @safe-global/theme to transpilePackages
and prioritize web app node_modules for module resolution so workspace
packages can resolve @mui/material.

---------

* refactor(web): inline support-chat-embed and use feature architecture

Remove @safe-global/support-chat-embed npm dependency (private GitHub
Package Registry) and inline the source directly as a feature module.
This eliminates the GITHUB_TOKEN requirement for yarn install.

- Add FEATURES.SUPPORT_CHAT chain feature flag
- Create src/features/support-chat/ with standard feature architecture
- Remove GitHub Package Registry config from .yarnrc.yml and .npmrc
- Remove react/react-dom webpack aliases from next.config.mjs
- Remove env-var based SUPPORT_CHAT_ENABLED constant

* fix(web): harden support-chat origin validation, rate limit, and data minimization

- Remove unconditional localhost origin bypass in postMessage listener
  (only allow localhost when chatOrigin itself is localhost)
- Tighten rate limit from 100 to 10 messages/second
- Use only safeAddress for email alias, don't leak signer EOA address

* chore: remove empty .npmrc

No longer needed after removing GitHub Package Registry dependency.

* fix: button css

---------

Co-authored-by: Rudy | Safe <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-03-31 09:34:35 +02:00

185 lines
5.4 KiB
JavaScript

import path from 'path'
import withBundleAnalyzer from '@next/bundle-analyzer'
import withPWAInit from '@ducanh2912/next-pwa'
import remarkGfm from 'remark-gfm'
import remarkHeadingId from 'remark-heading-id'
import createMDX from '@next/mdx'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { readFile } from 'fs/promises'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
import { SriManifestWebpackPlugin } from './plugins/sri-manifest-webpack-plugin.mjs'
let withRspack = null
if (process.env.USE_RSPACK === '1') {
process.env.NEXT_RSPACK = 'true'
// Disable rspack config validation to avoid warnings, use 'loose' to log errors.
process.env.RSPACK_CONFIG_VALIDATE = 'loose-silent'
delete process.env.TURBOPACK
try {
withRspack = (await import('next-rspack')).default
} catch {}
}
const SERVICE_WORKERS_PATH = './src/service-workers'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkgPath = path.join(__dirname, 'package.json')
const data = await readFile(pkgPath, 'utf-8')
const pkg = JSON.parse(data)
let commitHash = process.env.NEXT_PUBLIC_COMMIT_HASH
if (!commitHash) {
try {
commitHash = execSync('git rev-parse --short HEAD').toString().trim()
} catch {
commitHash = ''
}
}
const withPWA = withPWAInit({
dest: 'public',
workboxOptions: {
mode: 'production',
},
reloadOnOnline: false,
publicExcludes: [],
buildExcludes: [/./],
customWorkerSrc: SERVICE_WORKERS_PATH,
// Prefer InjectManifest for Web Push
swSrc: `${SERVICE_WORKERS_PATH}/index.ts`,
runtimeCaching: [
{
urlPattern: /\.(js|css|png|jpg|jpeg|gif|webp|svg|ico|ttf|woff|woff2|eot)$/,
handler: 'CacheFirst',
options: {
cacheName: 'static-assets',
expiration: {
maxEntries: 1000,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
},
},
],
cacheId: pkg.version,
})
const isProd = process.env.NODE_ENV === 'production'
const enableExperimentalOptimizations = process.env.ENABLE_EXPERIMENTAL_OPTIMIZATIONS === '1'
let appVersion = pkg.version
// Pin volatile values for visual regression builds to avoid Chromatic diffs
if (process.env.VISUAL_REGRESSION_BUILD === 'true') {
commitHash = 'vistest'
appVersion = 'istest' // UI prepends 'v' → displays 'vistest'
}
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // static site export
transpilePackages: ['@safe-global/store', '@safe-global/theme'],
images: {
unoptimized: true,
},
env: {
NEXT_PUBLIC_COMMIT_HASH: commitHash,
NEXT_PUBLIC_APP_VERSION: process.env.VISUAL_REGRESSION_BUILD === 'true' ? 'vistest' : pkg.version,
NEXT_PUBLIC_APP_HOMEPAGE: pkg.homepage,
VISUAL_REGRESSION_BUILD: process.env.VISUAL_REGRESSION_BUILD || '',
},
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
reactStrictMode: false,
productionBrowserSourceMaps: true,
eslint: {
dirs: ['src', 'cypress'],
},
...(isProd || enableExperimentalOptimizations
? {
experimental: {
optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns', '@gnosis.pm/zodiac'],
},
}
: {}),
webpack(config, { dev }) {
config.module.rules.push({
test: /\.svg$/i,
issuer: { and: [/\.(js|ts|md)x?$/] },
use: [
{
loader: '@svgr/webpack',
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: { removeViewBox: false },
},
},
],
},
titleProp: true,
},
},
],
})
config.resolve.alias = {
...config.resolve.alias,
'bn.js': path.resolve('../../node_modules/bn.js/lib/bn.js'),
'mainnet.json': path.resolve('../..node_modules/@ethereumjs/common/dist.browser/genesisStates/mainnet.json'),
'@mui/material$': path.resolve('./src/components/common/Mui'),
}
if (dev) {
config.optimization.splitChunks = {
...config.optimization.splitChunks,
cacheGroups: {
...config.optimization.splitChunks.cacheGroups,
customModule: {
test: /[\\/]..[\\/]..[\\/]node_modules[\\/](@safe-global|ethers)[\\/]/,
name: 'protocol-kit-ethers',
chunks: 'all',
},
},
}
config.optimization.minimize = false
}
// Add SRI manifest plugin (production only, skip for Cypress tests)
if (!dev && process.env.NODE_ENV !== 'cypress') {
config.plugins.push(new SriManifestWebpackPlugin())
}
return config
},
}
const isRspack = process.env.USE_RSPACK === '1'
const enablePWA = process.env.ENABLE_PWA === '1'
const withMDX = isRspack
? createMDX({ extension: /\.(md|mdx)?$/, jsx: true, options: {} })
: createMDX({
extension: /\.(md|mdx)?$/,
jsx: true,
options: {
remarkPlugins: [remarkFrontmatter, [remarkMdxFrontmatter, { name: 'metadata' }], remarkHeadingId, remarkGfm],
rehypePlugins: [],
},
})
const shouldEnablePWA = isProd || enablePWA
let config = shouldEnablePWA ? withPWA(withMDX(nextConfig)) : withMDX(nextConfig)
if (withRspack) config = withRspack(config)
export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true' })(config)