The dust filter hid any token with fiatBalance < $0.01. On chains with no
fiat price oracle the Client Gateway returns fiatConversion "0" (and thus
fiatBalance "0") for every token — Lux/Zoo/Pars C-Chains — so the filter
wrongly hid the entire native balance. safe.lux.network showed the Lux
Foundation treasury (0xE54cAf7E, ~994.7B LUX) as an empty "1 small balance"
instead of its real holdings.
A token is only dust if we can price it. Keep tokens whose price is unknown
(fiatConversion <= 0) and the native token; only hide genuinely-priced dust.
Decomplected: the two duplicate filterDustTokens copies (useVisibleBalances,
useHiddenTokenCounts) are now one price-aware helper in utils/balances.ts, so
the assets table and the "N small balances" indicator can never disagree.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pixel-sampling the rendered dashboard showed the only visible green left was the
active-nav bg (--sidebar-accent #f0fdf4). Neutralize it plus the remaining
non-dashboard green/teal tints (#008c73, #dcfce7, #1c5538, etc.) so no green can
surface in any state. Zero green remains except Google's OAuth logo.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Remaining green was in the sidebar active-nav accent (#22c55e), success/synced
tokens (#00b460/#16a34a and palette variants), and green fills baked into
public SVG illustrations + the MUI palettes — not the tokens already fixed.
Neutralize every green (brand accent, success, illustration) to grey across
apps/web/src, apps/web/public/images, and packages/theme (palettes + vars.css
regenerated). Keep only Google's brand green in the OAuth sign-in logo.
Verified: zero green remains except google.svg.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Browsers auto-request /favicon.ico (was the 15406B Safe icon). Replace it +
all brand/*/favicon.ico + android-chrome PNGs with a Lux monochrome mark.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The rendered Safe{Wallet}/Safe-Labs mark lived in SVG assets imported as
components (SidebarTopBar chrome, welcome/NewSafe wordmark, onboarding/WC/
first-tx icons) — not gated by isOfficialHost, so an HTML grep was clean but
the page visually showed it. Swap the assets themselves:
- logo-no-text.svg -> Lux mono rounded square (chrome icon)
- logo-safe-labs.svg-> Lux Safe wordmark (welcome/header)
- logo.svg -> Lux Safe wordmark (large)
- logo-round.svg -> Lux mono circle (was #12FF80 green)
All self-adapting (black in light / white in dark). Zero Safe mark, zero green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v2.2.0 left green in the sidebar-active accent, brand icons, and 4 Tailwind
notification badges because they read palette static.textBrand (#12FF80) and
hardcoded rgba(18,255,128,.1) — not the secondary token. Neutralize:
- static.ts textBrand -> #636669 (constant grey, reads on light+dark)
- regenerate vars.css from palettes
- badge bg rgba(18,255,128,.1) -> rgba(99,102,105,.15)
Zero green remains in shipped source.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
- yarn after-install lives in apps/web -> run it workspace-scoped (root has
no such script; typechain generate-types is required by the build)
- drop arm64 (QEMU-emulated Node build, no arm64 runner); lux-k8s is amd64
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The safe.lux.network host was serving a separate marketing landing
(lux-apps/safe, @luxfi/safe) as ghcr.io/luxfi/lux-safe:v2.0.0 — the real
@safe-global/web app was never deployed. Point the lux brand at the new Lux
Client Gateway (safe-cgw.lux.network) so chains 96369/200200/494949 and their
Safes load, and add a build-time-correct multi-arch image + CI.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Each per-brand manifest references brand/active/{logo.svg,favicon.ico,
android-chrome-{192,512}x{192,512}.png}. Without those files the runtime
manifest fetch 404s after select-brand. Stamps a 1x1 transparent PNG
placeholder per slot plus a text-only SVG logo so non-Safe builds resolve;
real artwork tracked in HANZO_WHITELABEL.md "manual review queue".
Also re-aligns brand/safe/manifest.json with the committed upstream
public/safe.webmanifest so build:safe leaves the working tree clean
(no spurious diff on the canonical wallet build).
Pass-1's URL codemod missed seven JSX/template-string call sites that
hardcoded https://help.safe.global directly. Route each through
${brand.helpUrl}/articles/... so non-Safe branded builds point at their
own help center (docs.lux.network/safe, docs.hanzo.ai/vault, etc).
No protocol identifiers changed; no test fixtures changed; no behavior
diff on a build:safe build.
apps/web/package.json gains build:safe / build:lux / build:hanzo
scripts. Each runs scripts/select-brand.sh <slug> (which copies
.env.<slug> -> .env.local, brand/<slug>/ -> brand/active/, and the
per-brand manifest.json over safe.webmanifest) then `yarn build`.
scripts/codemod-brand-urls.mjs is the Pass 1 driver kept in-tree so
future merges with upstream can re-run it. .gitignore covers the
brand/active output dir.
HANZO_WHITELABEL.md updated with the per-brand invocation, the
canonical pattern table, the protocol-vs-brand boundary, and how
to add a new brand (env + asset dir + one package.json script — no
source edits).
apps/web/public/brand/{safe,lux,hanzo}/manifest.json hold per-brand web
app manifests; scripts/select-brand.sh copies the chosen one over
apps/web/public/safe.webmanifest at build time. MetaTags og/twitter
image and twitter:site read brand.appUrl / brand.twitterUrl. The
default webmanifest description is now brand-neutral.
apps/mobile/app.config.ts derives the product name from
EXPO_PUBLIC_BRAND_NAME (default 'Safe{Mobile}') and threads it into
the dev variant prefix and the camera-permission rationale string,
removing the only hard-coded brand strings in mobile config.
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.
White-label plan for this fork: every hardcoded "Safe" / "safe.global"
reference in app code gets replaced with a read from a single Brand
config object sourced from env at build time. ~1,759 source files
carry one or more brand references; HANZO_WHITELABEL.md inventories
the patterns + phases the rewrite.
The same fork can then ship as Lux Safe, Hanzo Vault, partner builds,
etc. without forking the source per brand.
* chore: pin GitHub Actions to commit SHAs
Pin every third-party action under .github/workflows and .github/actions
to a 40-char commit SHA with a trailing version comment, per GitHub's
security hardening guidance.
anthropics/agent-action is bumped from the (long-stale) @beta tag
to v1.0.99, which also requires renaming the `direct_prompt` input to
`prompt`. chromaui/action is moved off the floating @latest tag onto
v16.9.1.
Resolves: WA-2301
* chore: bump npmMinumalAgeGate to 7 days
* chore: set chromaui/action to v 16.8.0
Schema regen brings new optional fields (UserSession.email, MemberUser.email)
and required createdByUserId/lastUpdatedByUserId on SpaceAddressBookItemDto.
Address book consumers that build entries from local data now pass 0 for the
new ID fields.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
* fix: drop console-sourced errors from Datadog RUM
The RUM SDK auto-instruments console.error via trackConsoleError with no
init flag to disable it, so every catch-block console.error in the app
(clipboard denial, RPC retries, third-party widget init, observability
self-recovery, etc.) lands as a RUM error and burns the Error-Free Views
SLO. PR #7674 only redirected logError() to warn level; the dozens of
direct console.error sites still slipped through.
Filter events with error.source === 'console' in beforeSend. Genuine
user-impacting failures continue to flow through trackError /
captureException (source: 'custom' / 'source'), which the filter keeps.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
* fix: also drop Browser Reporting API events (source=report) from RUM
Production data over a 7h window (2026-05-06 01:14-08:36 UTC, 5000
events) shows source=report dominates RUM error volume at 4095/5000
(82%), nearly all "csp_violation: 'eval' blocked by 'script-src'
directive" emitted by ReportingObserver from third-party scripts on
the welcome page. Console-source noise is 11%, unhandled exceptions
6%, real trackError 1%.
CSP/Reporting-API events are a security/policy signal, not a
user-impacting failure - they belong on a report-uri/report-to
endpoint, not the Error-Free Views SLO. Drop them with the same filter
mechanism as console-source. Combined with the previous commit, this
removes ~93% of current RUM error volume while preserving genuine
unhandled exceptions, network errors, and trackError signals.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
---------
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
* fix(mobile): never auto-redirect to Settings on biometrics toggle (WA-2239)
Mirrors the camera permission fix (WA-2229 / #7810). enableBiometrics no
longer calls openBiometricSettings as a side effect. The hook now returns
a discriminated outcome (enabled | os-not-configured | cancelled | error)
and each surface — the Settings switch and the biometrics opt-in screen —
renders an in-app alert with an explicit "Open Settings" button. Linking
is reachable only from a user-tapped button.
* refactor(mobile): handle biometrics errors + dedup setup prompt (WA-2239)
Addresses code review on PR #7811:
- Hook exposes promptBiometricsSetup() so the os-not-configured alert
lives once next to openBiometricSettings, with both consumers shrinking
to a single-line invocation.
- enableBiometrics no longer swallows unexpected errors silently: both the
Settings switch and the biometrics-opt-in screen now surface a toast or
Alert and Logger.error the underlying cause.
- disableBiometrics returns { status: 'error', error } when keychain
reset fails so callers can react; Redux still flips to disabled to match
user intent.
- Cancellation path now awaits Keychain.resetGenericPassword inside a
try/catch instead of fire-and-forget; useLayoutEffect awaits
disableBiometrics and catches rejections.
- Tests: assert exact error payload on the unexpected-error and verify
paths; new disable-failure test; promptBiometricsSetup wires the
Open Settings button to Linking.openURL only on tap.
* chore(mobile): remove stale hasInteracted comment in useBiometrics
The comment described a ref that was never added (introduced in #6059,
no corresponding code). It has misled readers into looking for state
that does not exist.
* fix(mobile): never auto-redirect to Settings on notification permission denial
Mirror the camera-permission fix (#7810). Apple guideline 5.1.1(iv) forbids
auto-redirecting to Settings as a side effect of a permission denial.
- NotificationService.openDeviceSettings: drop the internal notifee.requestPermission
so it can never chain a prompt denial into a Settings open. Pure side-effect now.
- NotificationService.getAllPermissions: drop the shouldOpenSettings parameter and
the auto-redirect branch. Returns status only; opening Settings is the caller's job.
- useNotificationManager: drop openSettingsOnDenied from requestAndRegister; on denial,
route both toggleNotificationState and enableNotification (threshold path) through
the existing asyncAlert (requestPushNotificationsPermission), where the user must
tap "Turn on" explicitly to open Settings.
- Add compliance unit tests asserting the core invariant: requestPermission -> denied
never calls Linking.openURL / Linking.openSettings, and openDeviceSettings does not
re-request permission.
WA-2238
* fix(mobile): only show Settings explainer on actual permission denial
Codex review on #7812: requestAndRegister returned false in two cases — permission
denied AND permission granted with a registration failure (network/backend). The
previous commit treated both as denial and pushed the user to Settings, which is
misleading when permission is already granted and leaves pendingPermissionRequestRef
stuck true.
Return a discriminated { success, permission } from requestAndRegister so
toggleNotificationState can gate the explainer Alert on permission === 'denied'.
Apple 5.1.1(iv) compliance is unchanged: Settings is still only opened from an
explicit "Turn on" tap inside the Alert.
Add a regression test covering the granted-permission registration-failure case.