mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-07-26 22:53:37 +00:00
feat(brand): create @safe-global/brand workspace
Exposes a Brand type and a default brand object whose values match upstream Safe strings. Per-brand builds override via NEXT_PUBLIC_BRAND_* (web) or EXPO_PUBLIC_BRAND_* (mobile) env vars. Single source of truth for product name, URLs, support contacts, and asset paths. No 'Safe' / 'safe.global' literal lives in apps/ or packages/*/src/ once the codemod passes land.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@safe-global/brand/*": ["../../packages/brand/src/*"],
|
||||
"@safe-global/store/*": ["../../packages/store/src/*"],
|
||||
"@safe-global/utils/*": ["../../packages/utils/src/*"],
|
||||
"@safe-global/test/*": ["../../config/test/*"]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# @safe-global/brand
|
||||
|
||||
White-label brand tokens for the Safe wallet monorepo.
|
||||
|
||||
## Why
|
||||
|
||||
The Safe wallet codebase is the upstream OSS reference. Lux Safe, Hanzo Vault,
|
||||
and future partner builds all ship from the same source. To keep them in lock-
|
||||
step we keep brand strings (product name, URLs, support contacts, asset paths,
|
||||
primary color) out of the source — they live here, in one file, behind one
|
||||
type — and per-brand builds override them via env vars.
|
||||
|
||||
## Shape
|
||||
|
||||
```ts
|
||||
import { brand } from '@safe-global/brand'
|
||||
|
||||
brand.name // "Safe{Wallet}" (or "Lux Safe", "Hanzo Vault", …)
|
||||
brand.domain // "safe.global"
|
||||
brand.email // "support@safe.global"
|
||||
brand.helpUrl // "https://help.safe.global"
|
||||
brand.logoUrl // "/images/safe-logo-green.png"
|
||||
brand.primaryColor // "#12FF80"
|
||||
```
|
||||
|
||||
See [`src/index.ts`](src/index.ts) for the full `Brand` interface.
|
||||
|
||||
## Setting brand values at build time
|
||||
|
||||
Web uses `NEXT_PUBLIC_BRAND_*`, mobile uses `EXPO_PUBLIC_BRAND_*`. The brand
|
||||
module reads whichever is set, so a single env file can drive both apps.
|
||||
|
||||
```bash
|
||||
# apps/web/.env.lux
|
||||
NEXT_PUBLIC_BRAND_NAME="Lux Safe"
|
||||
NEXT_PUBLIC_BRAND_DOMAIN="safe.lux.network"
|
||||
NEXT_PUBLIC_BRAND_EMAIL="support@lux.network"
|
||||
NEXT_PUBLIC_BRAND_HELP_URL="https://help.lux.network"
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL="/brand/lux/logo.svg"
|
||||
```
|
||||
|
||||
The repo ships three sample env files in `apps/web/`:
|
||||
|
||||
- `.env.safe` — upstream Safe defaults (sanity check)
|
||||
- `.env.lux` — Lux Safe
|
||||
- `.env.hanzo` — Hanzo Vault
|
||||
|
||||
`scripts/select-brand.sh <slug>` copies the selected env file and the matching
|
||||
asset directory into place, then the normal Next build picks them up.
|
||||
|
||||
## Rule
|
||||
|
||||
There is exactly one way to render the product name. It is `brand.name`. There
|
||||
is no `'Safe'` string literal in source files. If you find one, replace it.
|
||||
@@ -0,0 +1,20 @@
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['dist', 'node_modules'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/src/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@safe-global/brand",
|
||||
"description": "White-label brand tokens. One source of truth for product name, URLs, support contacts, and asset paths across web and mobile.",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"prettier": "prettier --check . --config ../../.prettierrc --ignore-path ../../.prettierignore",
|
||||
"prettier:fix": "prettier --write . --config ../../.prettierrc --ignore-path ../../.prettierignore"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"eslint": "^9.29.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.31.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { brand, type Brand } from './index'
|
||||
|
||||
describe('brand', () => {
|
||||
it('exposes the upstream Safe defaults when no env vars are set', () => {
|
||||
expect(brand.name).toBe('Safe{Wallet}')
|
||||
expect(brand.shortName).toBe('Safe')
|
||||
expect(brand.domain).toBe('safe.global')
|
||||
expect(brand.email).toBe('support@safe.global')
|
||||
expect(brand.helpUrl).toBe('https://help.safe.global')
|
||||
})
|
||||
|
||||
it('matches the Brand interface', () => {
|
||||
const b: Brand = brand
|
||||
expect(typeof b.name).toBe('string')
|
||||
expect(typeof b.shortName).toBe('string')
|
||||
expect(typeof b.domain).toBe('string')
|
||||
expect(typeof b.email).toBe('string')
|
||||
expect(typeof b.logoUrl).toBe('string')
|
||||
})
|
||||
|
||||
it('exposes every documented URL', () => {
|
||||
const required: Array<keyof Brand> = [
|
||||
'helpUrl',
|
||||
'termsUrl',
|
||||
'privacyUrl',
|
||||
'imprintUrl',
|
||||
'cookieUrl',
|
||||
'licensesUrl',
|
||||
'statusUrl',
|
||||
'developerUrl',
|
||||
'gatewayUrl',
|
||||
'discordUrl',
|
||||
'twitterUrl',
|
||||
]
|
||||
for (const key of required) {
|
||||
expect(brand[key]).toMatch(/^https?:\/\//)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @safe-global/brand — white-label brand tokens
|
||||
*
|
||||
* One source of truth for the human-facing brand. Every wallet shipped from
|
||||
* this monorepo reads these values; no `'Safe'` / `'safe.global'` literal
|
||||
* lives in `apps/` or `packages/*` source.
|
||||
*
|
||||
* Defaults match upstream Safe values, so an unbranded build is byte-for-byte
|
||||
* the same as today. Per-brand builds override via `NEXT_PUBLIC_BRAND_*`
|
||||
* (web) or `EXPO_PUBLIC_BRAND_*` (mobile) env vars at build time.
|
||||
*/
|
||||
|
||||
export interface Brand {
|
||||
/** Product name as it appears in chrome, titles, marketing copy. */
|
||||
name: string
|
||||
/** Short form for tight UI (sidebar, badges). */
|
||||
shortName: string
|
||||
/** Apex domain — `safe.global`, `safe.lux.network`, `vault.hanzo.ai`. */
|
||||
domain: string
|
||||
/** App host — `app.safe.global`, `safe.lux.network`. */
|
||||
appHost: string
|
||||
/** Full app URL with scheme. */
|
||||
appUrl: string
|
||||
/** Support email. */
|
||||
email: string
|
||||
/** Help center / docs URL (no trailing slash). */
|
||||
helpUrl: string
|
||||
/** Terms of service URL. */
|
||||
termsUrl: string
|
||||
/** Privacy policy URL. */
|
||||
privacyUrl: string
|
||||
/** Imprint / legal URL. */
|
||||
imprintUrl: string
|
||||
/** Cookie policy URL. */
|
||||
cookieUrl: string
|
||||
/** Licenses page URL. */
|
||||
licensesUrl: string
|
||||
/** Brand logo path (served by the app, swap per brand). */
|
||||
logoUrl: string
|
||||
/** Favicon path. */
|
||||
faviconUrl: string
|
||||
/** Primary brand color (hex). */
|
||||
primaryColor: string
|
||||
/** Status page URL. */
|
||||
statusUrl: string
|
||||
/** Developer portal URL. */
|
||||
developerUrl: string
|
||||
/** Gateway service URL (CGW — Safe Transaction Service). */
|
||||
gatewayUrl: string
|
||||
/** Staging gateway URL. */
|
||||
gatewayStagingUrl: string
|
||||
/** Discord / chat invite URL. */
|
||||
discordUrl: string
|
||||
/** Twitter / X URL. */
|
||||
twitterUrl: string
|
||||
/** Support chat alias domain (Pylon). */
|
||||
supportChatAliasDomain: string
|
||||
/** GitHub repo URL. */
|
||||
githubUrl: string
|
||||
/** Apple App Store URL (mobile). */
|
||||
appStoreUrl: string
|
||||
/** Google Play Store URL (mobile). */
|
||||
playStoreUrl: string
|
||||
/** Help article slug prefix (legacy `articles/` path). */
|
||||
helpArticlesPath: string
|
||||
}
|
||||
|
||||
const env = (key: string, fallback: string): string => {
|
||||
// Read NEXT_PUBLIC_* (web) and EXPO_PUBLIC_* (mobile) interchangeably so the
|
||||
// same brand bundle works in both apps.
|
||||
const next = `NEXT_PUBLIC_BRAND_${key}`
|
||||
const expo = `EXPO_PUBLIC_BRAND_${key}`
|
||||
return process.env[next] ?? process.env[expo] ?? fallback
|
||||
}
|
||||
|
||||
export const brand: Brand = {
|
||||
name: env('NAME', 'Safe{Wallet}'),
|
||||
shortName: env('SHORT_NAME', 'Safe'),
|
||||
domain: env('DOMAIN', 'safe.global'),
|
||||
appHost: env('APP_HOST', 'app.safe.global'),
|
||||
appUrl: env('APP_URL', 'https://app.safe.global'),
|
||||
email: env('EMAIL', 'support@safe.global'),
|
||||
helpUrl: env('HELP_URL', 'https://help.safe.global'),
|
||||
termsUrl: env('TERMS_URL', 'https://safe.global/terms'),
|
||||
privacyUrl: env('PRIVACY_URL', 'https://safe.global/privacy'),
|
||||
imprintUrl: env('IMPRINT_URL', 'https://safe.global/imprint'),
|
||||
cookieUrl: env('COOKIE_URL', 'https://safe.global/cookie'),
|
||||
licensesUrl: env('LICENSES_URL', 'https://safe.global/licenses'),
|
||||
logoUrl: env('LOGO_URL', '/images/safe-logo-green.png'),
|
||||
faviconUrl: env('FAVICON_URL', '/favicon.ico'),
|
||||
primaryColor: env('PRIMARY_COLOR', '#12FF80'),
|
||||
statusUrl: env('STATUS_URL', 'https://status.safe.global'),
|
||||
developerUrl: env('DEVELOPER_URL', 'https://developer.safe.global'),
|
||||
gatewayUrl: env('GATEWAY_URL', 'https://safe-client.safe.global'),
|
||||
gatewayStagingUrl: env('GATEWAY_STAGING_URL', 'https://safe-client.staging.5afe.dev'),
|
||||
discordUrl: env('DISCORD_URL', 'https://chat.safe.global'),
|
||||
twitterUrl: env('TWITTER_URL', 'https://twitter.com/safe'),
|
||||
supportChatAliasDomain: env('SUPPORT_CHAT_ALIAS_DOMAIN', 'anon.safe.global'),
|
||||
githubUrl: env('GITHUB_URL', 'https://github.com/safe-global/safe-wallet-monorepo'),
|
||||
appStoreUrl: env('APP_STORE_URL', 'https://apps.apple.com/app/id1515759131'),
|
||||
playStoreUrl: env('PLAY_STORE_URL', 'https://play.google.com/store/apps/details?id=io.gnosis.safe'),
|
||||
helpArticlesPath: env('HELP_ARTICLES_PATH', 'articles'),
|
||||
}
|
||||
|
||||
export default brand
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../config/tsconfig/confs/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@safe-global/brand/*": ["../../packages/brand/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13068,6 +13068,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@safe-global/brand@workspace:packages/brand":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@safe-global/brand@workspace:packages/brand"
|
||||
dependencies:
|
||||
"@eslint/js": "npm:^9.18.0"
|
||||
"@types/jest": "npm:^29.5.14"
|
||||
eslint: "npm:^9.29.0"
|
||||
jest: "npm:^29.7.0"
|
||||
prettier: "npm:^3.6.2"
|
||||
ts-jest: "npm:^29.4.0"
|
||||
typescript: "npm:~5.9.2"
|
||||
typescript-eslint: "npm:^8.31.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@safe-global/mobile@workspace:apps/mobile":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@safe-global/mobile@workspace:apps/mobile"
|
||||
@@ -29215,7 +29230,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"handlebars@npm:^4.7.8":
|
||||
"handlebars@npm:^4.7.8, handlebars@npm:^4.7.9":
|
||||
version: 4.7.9
|
||||
resolution: "handlebars@npm:4.7.9"
|
||||
dependencies:
|
||||
@@ -40736,6 +40751,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.7.4":
|
||||
version: 7.7.4
|
||||
resolution: "semver@npm:7.7.4"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"send@npm:0.19.0":
|
||||
version: 0.19.0
|
||||
resolution: "send@npm:0.19.0"
|
||||
@@ -43327,6 +43351,46 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-jest@npm:^29.4.0":
|
||||
version: 29.4.9
|
||||
resolution: "ts-jest@npm:29.4.9"
|
||||
dependencies:
|
||||
bs-logger: "npm:^0.2.6"
|
||||
fast-json-stable-stringify: "npm:^2.1.0"
|
||||
handlebars: "npm:^4.7.9"
|
||||
json5: "npm:^2.2.3"
|
||||
lodash.memoize: "npm:^4.1.2"
|
||||
make-error: "npm:^1.3.6"
|
||||
semver: "npm:^7.7.4"
|
||||
type-fest: "npm:^4.41.0"
|
||||
yargs-parser: "npm:^21.1.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ">=7.0.0-beta.0 <8"
|
||||
"@jest/transform": ^29.0.0 || ^30.0.0
|
||||
"@jest/types": ^29.0.0 || ^30.0.0
|
||||
babel-jest: ^29.0.0 || ^30.0.0
|
||||
jest: ^29.0.0 || ^30.0.0
|
||||
jest-util: ^29.0.0 || ^30.0.0
|
||||
typescript: ">=4.3 <7"
|
||||
peerDependenciesMeta:
|
||||
"@babel/core":
|
||||
optional: true
|
||||
"@jest/transform":
|
||||
optional: true
|
||||
"@jest/types":
|
||||
optional: true
|
||||
babel-jest:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jest-util:
|
||||
optional: true
|
||||
bin:
|
||||
ts-jest: cli.js
|
||||
checksum: 10/f5e81b1e13fff08da5b92d5a72f984f3393f40df73a1ae54473a780436b95dddb1452c78256e6d70a701c09ea427449657a5fbb3d142dc7e7a82eb192e80c3db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-mixer@npm:^6.0.3":
|
||||
version: 6.0.4
|
||||
resolution: "ts-mixer@npm:6.0.4"
|
||||
|
||||
Reference in New Issue
Block a user