From 5b84fbddab94e140efe9f413770cdb33af9cc349 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Mon, 23 Mar 2026 16:01:52 +0100 Subject: [PATCH 01/16] add Cobe --- apps/frontend/src/components/Globe/index.tsx | 174 ++++++++++++++++++ .../src/sections/individuals/Hero/index.tsx | 35 +--- bun.lock | 3 + package.json | 1 + .../contracts/interfaces/IERC5267.ts | 2 +- .../token/ERC20/extensions/ERC20Permit.ts | 2 +- .../contracts/utils/cryptography/EIP712.ts | 2 +- .../contracts/MockERC20Permit.ts | 2 +- 8 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 apps/frontend/src/components/Globe/index.tsx diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx new file mode 100644 index 000000000..df97121ea --- /dev/null +++ b/apps/frontend/src/components/Globe/index.tsx @@ -0,0 +1,174 @@ +import createGlobe from "cobe"; +import { useEffect, useRef, useState } from "react"; +import ARS_ICON from "../../assets/coins/ARS.png"; +import BRL_ICON from "../../assets/coins/BRL.png"; +import COP_ICON from "../../assets/coins/COP.png"; +import EUR_ICON from "../../assets/coins/EU.png"; +import MXN_ICON from "../../assets/coins/MXN.png"; +import USD_ICON from "../../assets/coins/USD.png"; +import { prefersReducedMotion } from "../../constants/animations"; +import { cn } from "../../helpers/cn"; + +const CANVAS_SIZE = 480; +// Cobe renders the globe sphere at NDC radius 0.8 with scale=1, so pixel radius = 0.8 * (size/2) +const GLOBE_PIXEL_RADIUS = 0.8 * (CANVAS_SIZE / 2); +// Must match the theta passed to createGlobe +const GLOBE_THETA = 0.38; +// Must match the phi passed to createGlobe +const GLOBE_INITIAL_PHI = 0; + +const CURRENCY_MARKERS = [ + { currency: "usd", icon: USD_ICON, lat: 38.91, lng: -77.04 }, + { currency: "brl", icon: BRL_ICON, lat: -15.8, lng: -47.89 }, + { currency: "eur", icon: EUR_ICON, lat: 50.85, lng: 4.35 }, + { currency: "mxn", icon: MXN_ICON, lat: 19.43, lng: -99.13 }, + { currency: "cop", icon: COP_ICON, lat: 4.71, lng: -74.07 }, + { currency: "ars", icon: ARS_ICON, lat: -34.61, lng: -58.38 } +] as const; + +type ProjectedPoint = { x: number; y: number; visible: boolean }; + +// Matches cobe's shader coordinate system exactly: +// sphere point: px = -cos(lat)*cos(lng), py = sin(lat), pz = cos(lat)*sin(lng) +// rotation: screen = M(theta, phi) * p (derived from mat3 A(theta,phi) in cobe's GLSL) +function projectToScreen(lat: number, lng: number, phi: number): ProjectedPoint { + const latRad = (lat * Math.PI) / 180; + const lngRad = (lng * Math.PI) / 180; + + const px = -Math.cos(latRad) * Math.cos(lngRad); + const py = Math.sin(latRad); + const pz = Math.cos(latRad) * Math.sin(lngRad); + + const cosPhi = Math.cos(phi); + const sinPhi = Math.sin(phi); + const cosTheta = Math.cos(GLOBE_THETA); + const sinTheta = Math.sin(GLOBE_THETA); + + const sx = cosPhi * px + sinPhi * pz; + const sy = sinPhi * sinTheta * px + cosTheta * py - cosPhi * sinTheta * pz; + const sz = -sinPhi * cosTheta * px + sinTheta * py + cosPhi * cosTheta * pz; + + if (sz <= 0) return { visible: false, x: 0, y: 0 }; + + return { + visible: true, + x: CANVAS_SIZE / 2 + sx * GLOBE_PIXEL_RADIUS, + y: CANVAS_SIZE / 2 - sy * GLOBE_PIXEL_RADIUS + }; +} + +interface GlobeProps { + className?: string; +} + +export const Globe = ({ className }: GlobeProps) => { + const reducedMotion = prefersReducedMotion(); + const canvasRef = useRef(null); + const phiRef = useRef(GLOBE_INITIAL_PHI); + const isDraggingRef = useRef(false); + const lastPointerXRef = useRef(0); + const projectedRef = useRef(CURRENCY_MARKERS.map(() => ({ visible: false, x: 0, y: 0 }))); + const [markerPositions, setMarkerPositions] = useState( + CURRENCY_MARKERS.map(() => ({ visible: false, x: 0, y: 0 })) + ); + + useEffect(() => { + if (!canvasRef.current || reducedMotion) return; + let rafId: number; + const globe = createGlobe(canvasRef.current, { + arcColor: [0.3, 0.5, 1], + arcHeight: 0.3, + arcs: [{ from: [37.78, -122.44], to: [40.71, -74.01] }], + arcWidth: 0.5, + baseColor: [0.07, 0.23, 0.72], + dark: 1, + devicePixelRatio: window.devicePixelRatio, + diffuse: 1.2, + glowColor: [1, 1, 1], + height: CANVAS_SIZE * 2, + mapBrightness: 6, + mapSamples: 16000, + markerColor: [0.07, 0.23, 0.72], + markers: [], + phi: 0, + theta: 0.38, + width: CANVAS_SIZE * 2 + }); + const tick = () => { + if (!isDraggingRef.current) { + phiRef.current += 0.003; + } + globe.update({ phi: phiRef.current }); + projectedRef.current = CURRENCY_MARKERS.map(m => projectToScreen(m.lat, m.lng, phiRef.current)); + setMarkerPositions([...projectedRef.current]); + rafId = requestAnimationFrame(tick); + }; + rafId = requestAnimationFrame(tick); + return () => { + globe.destroy(); + cancelAnimationFrame(rafId); + }; + }, [reducedMotion]); + + const onPointerDown = (e: React.PointerEvent) => { + isDraggingRef.current = true; + lastPointerXRef.current = e.clientX; + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + }; + + const onPointerMove = (e: React.PointerEvent) => { + if (!isDraggingRef.current) return; + const delta = e.clientX - lastPointerXRef.current; + phiRef.current += (delta / CANVAS_SIZE) * Math.PI; + lastPointerXRef.current = e.clientX; + }; + + const onPointerUp = () => { + isDraggingRef.current = false; + }; + + if (reducedMotion) { + return ( +
+
+ {CURRENCY_MARKERS.map(m => ( + {m.currency.toUpperCase()} + ))} +
+
+ ); + } + + return ( +
+ +
+ {CURRENCY_MARKERS.map((m, i) => { + const pos = markerPositions[i]; + if (!pos?.visible) return null; + return ( + {m.currency.toUpperCase()} + ); + })} +
+
+ ); +}; diff --git a/apps/frontend/src/sections/individuals/Hero/index.tsx b/apps/frontend/src/sections/individuals/Hero/index.tsx index b6bf02bd0..b0c1e73b9 100644 --- a/apps/frontend/src/sections/individuals/Hero/index.tsx +++ b/apps/frontend/src/sections/individuals/Hero/index.tsx @@ -1,9 +1,8 @@ import { Link } from "@tanstack/react-router"; import { motion } from "motion/react"; import { Trans, useTranslation } from "react-i18next"; -import WidgetSnippetImage from "../../../assets/widget-snippet.png"; -import WidgetSnippetImageSell from "../../../assets/widget-snippet-sell.png"; import { AnimatedTitle } from "../../../components/AnimatedTitle"; +import { Globe } from "../../../components/Globe"; import { fadeInUp, prefersReducedMotion, staggerContainer } from "../../../constants/animations"; export const Hero = () => { @@ -66,39 +65,11 @@ export const Hero = () => {
-
- - -
-
+
diff --git a/bun.lock b/bun.lock index bd14017b3..10fbecef2 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "vortex-monorepo", "dependencies": { "big.js": "^7.0.1", + "cobe": "^2.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3", @@ -2315,6 +2316,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cobe": ["cobe@2.0.1", "", {}, "sha512-aaa6vcIlaC8C1SF50LDH0Anybo/EAXnrxqe+bwvr4+YUtZydqjeBjTTD7ziCCkbRrRGSns3I3F6cZsf3W+L+ag=="], + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], diff --git a/package.json b/package.json index 616f2492d..f66519d94 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "big.js": "^7.0.1", + "cobe": "^2.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3" diff --git a/relayer-contract/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts b/relayer-contract/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts index 4c8c1de04..1bb69a5af 100644 --- a/relayer-contract/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts +++ b/relayer-contract/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts @@ -34,7 +34,7 @@ export interface IERC5267Interface extends Interface { export namespace EIP712DomainChangedEvent { export type InputTuple = []; export type OutputTuple = []; - export interface OutputObject {} + export type OutputObject = {}; export type Event = TypedContractEvent; export type Filter = TypedDeferredTopicFilter; export type Log = TypedEventLog; diff --git a/relayer-contract/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts b/relayer-contract/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts index 5fe274bab..965ff39e5 100644 --- a/relayer-contract/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts +++ b/relayer-contract/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts @@ -92,7 +92,7 @@ export namespace ApprovalEvent { export namespace EIP712DomainChangedEvent { export type InputTuple = []; export type OutputTuple = []; - export interface OutputObject {} + export type OutputObject = {}; export type Event = TypedContractEvent; export type Filter = TypedDeferredTopicFilter; export type Log = TypedEventLog; diff --git a/relayer-contract/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts b/relayer-contract/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts index 892c84bea..946bec79a 100644 --- a/relayer-contract/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts +++ b/relayer-contract/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts @@ -34,7 +34,7 @@ export interface EIP712Interface extends Interface { export namespace EIP712DomainChangedEvent { export type InputTuple = []; export type OutputTuple = []; - export interface OutputObject {} + export type OutputObject = {}; export type Event = TypedContractEvent; export type Filter = TypedDeferredTopicFilter; export type Log = TypedEventLog; diff --git a/relayer-contract/typechain-types/contracts/MockERC20Permit.ts b/relayer-contract/typechain-types/contracts/MockERC20Permit.ts index 454094882..a95156ec0 100644 --- a/relayer-contract/typechain-types/contracts/MockERC20Permit.ts +++ b/relayer-contract/typechain-types/contracts/MockERC20Permit.ts @@ -95,7 +95,7 @@ export namespace ApprovalEvent { export namespace EIP712DomainChangedEvent { export type InputTuple = []; export type OutputTuple = []; - export interface OutputObject {} + export type OutputObject = {}; export type Event = TypedContractEvent; export type Filter = TypedDeferredTopicFilter; export type Log = TypedEventLog; From 495e2669c92309f6876f282798dbbddc60c2a6c6 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Mon, 23 Mar 2026 16:53:07 +0100 Subject: [PATCH 02/16] fix cobe position --- apps/frontend/src/components/Globe/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index df97121ea..a99d9e49e 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -78,7 +78,9 @@ export const Globe = ({ className }: GlobeProps) => { const globe = createGlobe(canvasRef.current, { arcColor: [0.3, 0.5, 1], arcHeight: 0.3, - arcs: [{ from: [37.78, -122.44], to: [40.71, -74.01] }], + arcs: CURRENCY_MARKERS.flatMap((a, i) => + CURRENCY_MARKERS.slice(i + 1).map(b => ({ from: [a.lat, a.lng], to: [b.lat, b.lng] })) + ), arcWidth: 0.5, baseColor: [0.07, 0.23, 0.72], dark: 1, @@ -99,7 +101,7 @@ export const Globe = ({ className }: GlobeProps) => { phiRef.current += 0.003; } globe.update({ phi: phiRef.current }); - projectedRef.current = CURRENCY_MARKERS.map(m => projectToScreen(m.lat, m.lng, phiRef.current)); + projectedRef.current = CURRENCY_MARKERS.map(m => projectToScreen(m.lat, m.lng, phiRef.current + Math.PI)); setMarkerPositions([...projectedRef.current]); rafId = requestAnimationFrame(tick); }; From 1b33539335b71d701fe04131d73216d68c61ef06 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Tue, 24 Mar 2026 22:11:18 +0100 Subject: [PATCH 03/16] improve Cobe size --- apps/frontend/src/components/Globe/index.tsx | 30 ++++++------------- .../src/components/Navbar/DesktopNavbar.tsx | 4 +-- .../src/sections/individuals/Hero/index.tsx | 2 +- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index a99d9e49e..3f929c3d0 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -9,11 +9,11 @@ import USD_ICON from "../../assets/coins/USD.png"; import { prefersReducedMotion } from "../../constants/animations"; import { cn } from "../../helpers/cn"; -const CANVAS_SIZE = 480; +const CANVAS_SIZE = 780; // Cobe renders the globe sphere at NDC radius 0.8 with scale=1, so pixel radius = 0.8 * (size/2) const GLOBE_PIXEL_RADIUS = 0.8 * (CANVAS_SIZE / 2); // Must match the theta passed to createGlobe -const GLOBE_THETA = 0.38; +const GLOBE_THETA = -0.1; // Must match the phi passed to createGlobe const GLOBE_INITIAL_PHI = 0; @@ -77,23 +77,23 @@ export const Globe = ({ className }: GlobeProps) => { let rafId: number; const globe = createGlobe(canvasRef.current, { arcColor: [0.3, 0.5, 1], - arcHeight: 0.3, + arcHeight: 0.25, arcs: CURRENCY_MARKERS.flatMap((a, i) => CURRENCY_MARKERS.slice(i + 1).map(b => ({ from: [a.lat, a.lng], to: [b.lat, b.lng] })) ), - arcWidth: 0.5, + arcWidth: 0.3, baseColor: [0.07, 0.23, 0.72], dark: 1, devicePixelRatio: window.devicePixelRatio, diffuse: 1.2, - glowColor: [1, 1, 1], + glowColor: [0.07, 0.23, 0.72], height: CANVAS_SIZE * 2, - mapBrightness: 6, - mapSamples: 16000, + mapBrightness: 3, + mapSamples: 18000, markerColor: [0.07, 0.23, 0.72], markers: [], phi: 0, - theta: 0.38, + theta: GLOBE_THETA, width: CANVAS_SIZE * 2 }); const tick = () => { @@ -129,21 +129,9 @@ export const Globe = ({ className }: GlobeProps) => { isDraggingRef.current = false; }; - if (reducedMotion) { - return ( -
-
- {CURRENCY_MARKERS.map(m => ( - {m.currency.toUpperCase()} - ))} -
-
- ); - } - return (
{
- - Buy & Sell + + Open App
diff --git a/apps/frontend/src/sections/individuals/Hero/index.tsx b/apps/frontend/src/sections/individuals/Hero/index.tsx index b0c1e73b9..46a3c4a28 100644 --- a/apps/frontend/src/sections/individuals/Hero/index.tsx +++ b/apps/frontend/src/sections/individuals/Hero/index.tsx @@ -66,7 +66,7 @@ export const Hero = () => {
From b823521b5910e63f95172c39b7cbe929433fd831 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Tue, 31 Mar 2026 12:43:46 +0200 Subject: [PATCH 04/16] improve Globe responsiveness --- apps/frontend/src/components/Globe/index.tsx | 211 +++++++++++------- .../src/sections/individuals/Hero/index.tsx | 10 +- 2 files changed, 130 insertions(+), 91 deletions(-) diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index 3f929c3d0..6a264d02d 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -9,13 +9,16 @@ import USD_ICON from "../../assets/coins/USD.png"; import { prefersReducedMotion } from "../../constants/animations"; import { cn } from "../../helpers/cn"; -const CANVAS_SIZE = 780; -// Cobe renders the globe sphere at NDC radius 0.8 with scale=1, so pixel radius = 0.8 * (size/2) -const GLOBE_PIXEL_RADIUS = 0.8 * (CANVAS_SIZE / 2); -// Must match the theta passed to createGlobe const GLOBE_THETA = -0.1; -// Must match the phi passed to createGlobe const GLOBE_INITIAL_PHI = 0; +const NORMAL_SPEED = 0.003; +const GLOBE_COLOR: [number, number, number] = [0.07, 0.23, 0.72]; + +const GLOBE_SIZES = { + lg: 780, + md: 560, + sm: 380 +} as const; const CURRENCY_MARKERS = [ { currency: "usd", icon: USD_ICON, lat: 38.91, lng: -77.04 }, @@ -26,12 +29,45 @@ const CURRENCY_MARKERS = [ { currency: "ars", icon: ARS_ICON, lat: -34.61, lng: -58.38 } ] as const; -type ProjectedPoint = { x: number; y: number; visible: boolean }; +function getGlobeSize(): number { + if (window.matchMedia("(min-width: 1024px)").matches) return GLOBE_SIZES.lg; + if (window.matchMedia("(min-width: 640px)").matches) return GLOBE_SIZES.md; + return GLOBE_SIZES.sm; +} + +function createGlobeConfig(size: number) { + const bufferSize = size * window.devicePixelRatio; + return { + arcColor: [0.3, 0.5, 1] as [number, number, number], + arcHeight: 0.25, + arcs: CURRENCY_MARKERS.flatMap((a, i) => + CURRENCY_MARKERS.slice(i + 1).map(b => ({ + from: [a.lat, a.lng] as [number, number], + to: [b.lat, b.lng] as [number, number] + })) + ), + arcWidth: 0.3, + baseColor: GLOBE_COLOR, + dark: 1, + devicePixelRatio: window.devicePixelRatio, + diffuse: 1.2, + glowColor: GLOBE_COLOR, + height: bufferSize, + mapBrightness: 3, + mapSamples: 12000, + markerColor: GLOBE_COLOR, + markers: [], + phi: 0, + theta: GLOBE_THETA, + width: bufferSize + }; +} // Matches cobe's shader coordinate system exactly: -// sphere point: px = -cos(lat)*cos(lng), py = sin(lat), pz = cos(lat)*sin(lng) -// rotation: screen = M(theta, phi) * p (derived from mat3 A(theta,phi) in cobe's GLSL) -function projectToScreen(lat: number, lng: number, phi: number): ProjectedPoint { +// sphere point: px = -cos(lat)*cos(lng), py = sin(lat), pz = cos(lat)*sin(lng) +// rotation: screen = R_x(theta) * R_y(phi) * p (derived from mat3 A(theta,phi) in cobe's GLSL) +function projectToScreen(lat: number, lng: number, phi: number, size: number) { + const pixelRadius = 0.8 * (size / 2); const latRad = (lat * Math.PI) / 180; const lngRad = (lng * Math.PI) / 180; @@ -48,13 +84,51 @@ function projectToScreen(lat: number, lng: number, phi: number): ProjectedPoint const sy = sinPhi * sinTheta * px + cosTheta * py - cosPhi * sinTheta * pz; const sz = -sinPhi * cosTheta * px + sinTheta * py + cosPhi * cosTheta * pz; - if (sz <= 0) return { visible: false, x: 0, y: 0 }; + if (sz <= 0) return null; return { - visible: true, - x: CANVAS_SIZE / 2 + sx * GLOBE_PIXEL_RADIUS, - y: CANVAS_SIZE / 2 - sy * GLOBE_PIXEL_RADIUS + x: size / 2 + sx * pixelRadius, + y: size / 2 - sy * pixelRadius + }; +} + +function updateMarkers(phi: number, size: number, markerRefs: React.RefObject<(HTMLImageElement | null)[]>) { + for (let i = 0; i < CURRENCY_MARKERS.length; i++) { + const el = markerRefs.current[i]; + if (!el) continue; + const pos = projectToScreen(CURRENCY_MARKERS[i].lat, CURRENCY_MARKERS[i].lng, phi + Math.PI, size); + if (pos) { + el.style.display = ""; + el.style.left = `${pos.x}px`; + el.style.top = `${pos.y}px`; + } else { + el.style.display = "none"; + } + } +} + +function useDragRotation(phiRef: React.RefObject, size: number) { + const isDraggingRef = useRef(false); + const lastPointerXRef = useRef(0); + + const onPointerDown = (e: React.PointerEvent) => { + isDraggingRef.current = true; + lastPointerXRef.current = e.clientX; + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + }; + + const onPointerMove = (e: React.PointerEvent) => { + if (!isDraggingRef.current) return; + const delta = e.clientX - lastPointerXRef.current; + phiRef.current += (delta / size) * Math.PI; + lastPointerXRef.current = e.clientX; + }; + + const onPointerUp = () => { + isDraggingRef.current = false; }; + + return { isDraggingRef, onPointerCancel: onPointerUp, onPointerDown, onPointerMove, onPointerUp }; } interface GlobeProps { @@ -63,46 +137,34 @@ interface GlobeProps { export const Globe = ({ className }: GlobeProps) => { const reducedMotion = prefersReducedMotion(); + // size as state — triggers globe effect to recreate at new canvas dimensions on breakpoint change + const [size, setSize] = useState(getGlobeSize); + const canvasRef = useRef(null); + // positions written directly to DOM each frame, skipping React re-renders + const markerRefs = useRef<(HTMLImageElement | null)[]>(CURRENCY_MARKERS.map(() => null)); const phiRef = useRef(GLOBE_INITIAL_PHI); - const isDraggingRef = useRef(false); - const lastPointerXRef = useRef(0); - const projectedRef = useRef(CURRENCY_MARKERS.map(() => ({ visible: false, x: 0, y: 0 }))); - const [markerPositions, setMarkerPositions] = useState( - CURRENCY_MARKERS.map(() => ({ visible: false, x: 0, y: 0 })) - ); + + const dragHandlers = useDragRotation(phiRef, size); + + // Recreate globe when crossing Tailwind sm (640px) or lg (1024px) breakpoints + useEffect(() => { + const mq = window.matchMedia("(min-width: 1024px), (min-width: 640px)"); + const handler = () => setSize(getGlobeSize()); + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, []); useEffect(() => { if (!canvasRef.current || reducedMotion) return; let rafId: number; - const globe = createGlobe(canvasRef.current, { - arcColor: [0.3, 0.5, 1], - arcHeight: 0.25, - arcs: CURRENCY_MARKERS.flatMap((a, i) => - CURRENCY_MARKERS.slice(i + 1).map(b => ({ from: [a.lat, a.lng], to: [b.lat, b.lng] })) - ), - arcWidth: 0.3, - baseColor: [0.07, 0.23, 0.72], - dark: 1, - devicePixelRatio: window.devicePixelRatio, - diffuse: 1.2, - glowColor: [0.07, 0.23, 0.72], - height: CANVAS_SIZE * 2, - mapBrightness: 3, - mapSamples: 18000, - markerColor: [0.07, 0.23, 0.72], - markers: [], - phi: 0, - theta: GLOBE_THETA, - width: CANVAS_SIZE * 2 - }); + const globe = createGlobe(canvasRef.current, createGlobeConfig(size)); const tick = () => { - if (!isDraggingRef.current) { - phiRef.current += 0.003; + if (!dragHandlers.isDraggingRef.current) { + phiRef.current += NORMAL_SPEED; } globe.update({ phi: phiRef.current }); - projectedRef.current = CURRENCY_MARKERS.map(m => projectToScreen(m.lat, m.lng, phiRef.current + Math.PI)); - setMarkerPositions([...projectedRef.current]); + updateMarkers(phiRef.current, size, markerRefs); rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); @@ -110,54 +172,35 @@ export const Globe = ({ className }: GlobeProps) => { globe.destroy(); cancelAnimationFrame(rafId); }; - }, [reducedMotion]); - - const onPointerDown = (e: React.PointerEvent) => { - isDraggingRef.current = true; - lastPointerXRef.current = e.clientX; - (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); - }; - - const onPointerMove = (e: React.PointerEvent) => { - if (!isDraggingRef.current) return; - const delta = e.clientX - lastPointerXRef.current; - phiRef.current += (delta / CANVAS_SIZE) * Math.PI; - lastPointerXRef.current = e.clientX; - }; - - const onPointerUp = () => { - isDraggingRef.current = false; - }; + }, [reducedMotion, size, dragHandlers.isDraggingRef]); return (
- {CURRENCY_MARKERS.map((m, i) => { - const pos = markerPositions[i]; - if (!pos?.visible) return null; - return ( - {m.currency.toUpperCase()} - ); - })} + {CURRENCY_MARKERS.map((m, i) => ( + {m.currency.toUpperCase()} { + markerRefs.current[i] = el; + }} + src={m.icon} + style={{ display: "none", left: 0, top: 0 }} + width={32} + /> + ))}
); diff --git a/apps/frontend/src/sections/individuals/Hero/index.tsx b/apps/frontend/src/sections/individuals/Hero/index.tsx index 46a3c4a28..1ff757dfe 100644 --- a/apps/frontend/src/sections/individuals/Hero/index.tsx +++ b/apps/frontend/src/sections/individuals/Hero/index.tsx @@ -12,7 +12,7 @@ export const Hero = () => { return (
@@ -64,12 +64,8 @@ export const Hero = () => {
-
- +
+
From 5e8f6b890c1bcd017037a071020b35e469883afb Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Tue, 31 Mar 2026 12:58:00 +0200 Subject: [PATCH 05/16] fix Globe position --- apps/frontend/src/components/Globe/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index 6a264d02d..97d9a5d42 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -176,7 +176,7 @@ export const Globe = ({ className }: GlobeProps) => { return (
From 101c6e12117ac2ddd5dfd17c977afd44d6b0a216 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 1 Apr 2026 18:14:23 +0200 Subject: [PATCH 06/16] add vertical rotation, 2-axis drag, and fix DOM attribute leak --- apps/frontend/src/components/Globe/index.tsx | 65 ++++++++++--------- .../src/sections/individuals/Hero/index.tsx | 8 ++- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index 97d9a5d42..305c9d645 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -1,5 +1,5 @@ import createGlobe from "cobe"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useSyncExternalStore } from "react"; import ARS_ICON from "../../assets/coins/ARS.png"; import BRL_ICON from "../../assets/coins/BRL.png"; import COP_ICON from "../../assets/coins/COP.png"; @@ -9,15 +9,16 @@ import USD_ICON from "../../assets/coins/USD.png"; import { prefersReducedMotion } from "../../constants/animations"; import { cn } from "../../helpers/cn"; -const GLOBE_THETA = -0.1; -const GLOBE_INITIAL_PHI = 0; -const NORMAL_SPEED = 0.003; +const GLOBE_THETA = -0.08; +const GLOBE_INITIAL_PHI = -1.32; +const NORMAL_SPEED = 0.0005; +const VERTICAL_SPEED = -0.0005; const GLOBE_COLOR: [number, number, number] = [0.07, 0.23, 0.72]; const GLOBE_SIZES = { - lg: 780, - md: 560, - sm: 380 + lg: 960, + md: 780, + sm: 560 } as const; const CURRENCY_MARKERS = [ @@ -66,7 +67,7 @@ function createGlobeConfig(size: number) { // Matches cobe's shader coordinate system exactly: // sphere point: px = -cos(lat)*cos(lng), py = sin(lat), pz = cos(lat)*sin(lng) // rotation: screen = R_x(theta) * R_y(phi) * p (derived from mat3 A(theta,phi) in cobe's GLSL) -function projectToScreen(lat: number, lng: number, phi: number, size: number) { +function projectToScreen(lat: number, lng: number, phi: number, theta: number, size: number) { const pixelRadius = 0.8 * (size / 2); const latRad = (lat * Math.PI) / 180; const lngRad = (lng * Math.PI) / 180; @@ -77,8 +78,8 @@ function projectToScreen(lat: number, lng: number, phi: number, size: number) { const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); - const cosTheta = Math.cos(GLOBE_THETA); - const sinTheta = Math.sin(GLOBE_THETA); + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); const sx = cosPhi * px + sinPhi * pz; const sy = sinPhi * sinTheta * px + cosTheta * py - cosPhi * sinTheta * pz; @@ -92,11 +93,11 @@ function projectToScreen(lat: number, lng: number, phi: number, size: number) { }; } -function updateMarkers(phi: number, size: number, markerRefs: React.RefObject<(HTMLImageElement | null)[]>) { +function updateMarkers(phi: number, theta: number, size: number, markerRefs: React.RefObject<(HTMLImageElement | null)[]>) { for (let i = 0; i < CURRENCY_MARKERS.length; i++) { const el = markerRefs.current[i]; if (!el) continue; - const pos = projectToScreen(CURRENCY_MARKERS[i].lat, CURRENCY_MARKERS[i].lng, phi + Math.PI, size); + const pos = projectToScreen(CURRENCY_MARKERS[i].lat, CURRENCY_MARKERS[i].lng, phi + Math.PI, theta, size); if (pos) { el.style.display = ""; el.style.left = `${pos.x}px`; @@ -107,21 +108,24 @@ function updateMarkers(phi: number, size: number, markerRefs: React.RefObject<(H } } -function useDragRotation(phiRef: React.RefObject, size: number) { +function useDragRotation(phiRef: React.RefObject, thetaRef: React.RefObject, size: number) { const isDraggingRef = useRef(false); const lastPointerXRef = useRef(0); + const lastPointerYRef = useRef(0); const onPointerDown = (e: React.PointerEvent) => { isDraggingRef.current = true; lastPointerXRef.current = e.clientX; + lastPointerYRef.current = e.clientY; (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }; const onPointerMove = (e: React.PointerEvent) => { if (!isDraggingRef.current) return; - const delta = e.clientX - lastPointerXRef.current; - phiRef.current += (delta / size) * Math.PI; + phiRef.current += ((e.clientX - lastPointerXRef.current) / size) * Math.PI; + thetaRef.current += ((e.clientY - lastPointerYRef.current) / size) * Math.PI; lastPointerXRef.current = e.clientX; + lastPointerYRef.current = e.clientY; }; const onPointerUp = () => { @@ -131,40 +135,39 @@ function useDragRotation(phiRef: React.RefObject, size: number) { return { isDraggingRef, onPointerCancel: onPointerUp, onPointerDown, onPointerMove, onPointerUp }; } +function subscribeToBreakpoints(callback: () => void) { + const mq = window.matchMedia("(min-width: 1024px), (min-width: 640px)"); + mq.addEventListener("change", callback); + return () => mq.removeEventListener("change", callback); +} + interface GlobeProps { className?: string; } export const Globe = ({ className }: GlobeProps) => { const reducedMotion = prefersReducedMotion(); - // size as state — triggers globe effect to recreate at new canvas dimensions on breakpoint change - const [size, setSize] = useState(getGlobeSize); + const size = useSyncExternalStore(subscribeToBreakpoints, getGlobeSize, getGlobeSize); const canvasRef = useRef(null); // positions written directly to DOM each frame, skipping React re-renders const markerRefs = useRef<(HTMLImageElement | null)[]>(CURRENCY_MARKERS.map(() => null)); const phiRef = useRef(GLOBE_INITIAL_PHI); + const thetaRef = useRef(GLOBE_THETA); - const dragHandlers = useDragRotation(phiRef, size); - - // Recreate globe when crossing Tailwind sm (640px) or lg (1024px) breakpoints - useEffect(() => { - const mq = window.matchMedia("(min-width: 1024px), (min-width: 640px)"); - const handler = () => setSize(getGlobeSize()); - mq.addEventListener("change", handler); - return () => mq.removeEventListener("change", handler); - }, []); + const { isDraggingRef, ...dragHandlers } = useDragRotation(phiRef, thetaRef, size); useEffect(() => { if (!canvasRef.current || reducedMotion) return; let rafId: number; const globe = createGlobe(canvasRef.current, createGlobeConfig(size)); const tick = () => { - if (!dragHandlers.isDraggingRef.current) { + if (!isDraggingRef.current) { phiRef.current += NORMAL_SPEED; + thetaRef.current += VERTICAL_SPEED; } - globe.update({ phi: phiRef.current }); - updateMarkers(phiRef.current, size, markerRefs); + globe.update({ phi: phiRef.current, theta: thetaRef.current }); + updateMarkers(phiRef.current, thetaRef.current, size, markerRefs); rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); @@ -172,11 +175,11 @@ export const Globe = ({ className }: GlobeProps) => { globe.destroy(); cancelAnimationFrame(rafId); }; - }, [reducedMotion, size, dragHandlers.isDraggingRef]); + }, [reducedMotion, size]); return (
diff --git a/apps/frontend/src/sections/individuals/Hero/index.tsx b/apps/frontend/src/sections/individuals/Hero/index.tsx index 1ff757dfe..29d7987e4 100644 --- a/apps/frontend/src/sections/individuals/Hero/index.tsx +++ b/apps/frontend/src/sections/individuals/Hero/index.tsx @@ -14,7 +14,7 @@ export const Hero = () => { aria-label={t("pages.main.hero.title")} className="relative overflow-hidden bg-[radial-gradient(at_74%_98%,theme(colors.blue.900),theme(colors.blue.950),theme(colors.blue.950))] py-16 lg:py-42" > -
+
{
-
+
From bdd2edd8f0d874322dce1bd07798dbe58bfb251c Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 1 Apr 2026 18:30:07 +0200 Subject: [PATCH 07/16] fix vortex-primary-inverse button hover color --- apps/frontend/App.css | 56 +++++++------------------------------------ 1 file changed, 8 insertions(+), 48 deletions(-) diff --git a/apps/frontend/App.css b/apps/frontend/App.css index 377b3d0bb..b81ed9dff 100644 --- a/apps/frontend/App.css +++ b/apps/frontend/App.css @@ -161,12 +161,7 @@ } .btn-vortex-accent { - @apply bg-gray-200; - @apply text-gray-700; - @apply rounded-full; - @apply border; - @apply border-gray-300; - @apply duration-200; + @apply bg-gray-200 text-gray-700 rounded-full border border-gray-300; transition: scale 0.1s ease-in-out; } @@ -185,12 +180,7 @@ } .btn-vortex-success { - @apply bg-success; - @apply text-success-content; - @apply rounded-[var(--radius-field)]; - @apply border; - @apply border-success; - @apply duration-200; + @apply bg-success text-success-content rounded-[var(--radius-field)] border border-success; transition: scale 0.1s ease-in-out; } @@ -228,12 +218,7 @@ } .btn-vortex-primary-inverse { - @apply bg-white; - @apply text-primary; - @apply rounded-[var(--radius-field)]; - @apply border; - @apply border-primary; - @apply cursor-pointer; + @apply bg-white text-primary rounded-[var(--radius-field)] border border-primary cursor-pointer; transition: scale 0.1s ease-in-out; } @@ -242,31 +227,15 @@ } .btn-vortex-primary-inverse:hover { - @apply bg-primary/20; - @apply border-primary; + @apply bg-gray-300 border-primary text-primary; } .btn-vortex-primary-inverse:disabled { - @apply bg-white; - @apply text-primary; - @apply border-primary; - @apply opacity-40; - @apply cursor-not-allowed; -} - -.btn-vortex-primary-inverse:active, -.btn-vortex-primary-inverse:focus { - @apply bg-primary/20; - @apply text-primary; - @apply border-primary; + @apply bg-white text-primary border-primary opacity-40 cursor-not-allowed; } .btn-vortex-secondary { - @apply text-white; - @apply bg-pink-600; - @apply rounded-[var(--radius-field)]; - @apply border-pink-600; - @apply shadow-none; + @apply text-white bg-pink-600 rounded-[var(--radius-field)] border-pink-600 shadow-none; transition: scale 0.1s ease-in-out; } @@ -285,12 +254,7 @@ } .btn-vortex-danger { - @apply bg-error; - @apply text-error-content; - @apply rounded-xl; - @apply border; - @apply border-error; - @apply shadow-none; + @apply bg-error text-error-content rounded-xl border border-error shadow-none; transition: scale 0.1s ease-in-out; } @@ -305,11 +269,7 @@ } .btn-vortex-danger:disabled { - @apply bg-error; - @apply text-error-content; - @apply border-error; - @apply opacity-40; - @apply cursor-not-allowed; + @apply bg-error text-error-content border-error opacity-40 cursor-not-allowed; } .btn-vortex-danger:active, From 45a3fa61867279cf1b8f6254fd60004fe3797c7c Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 1 Apr 2026 18:39:25 +0200 Subject: [PATCH 08/16] address PR comments --- apps/frontend/package.json | 1 + apps/frontend/src/components/Globe/index.tsx | 11 ++++++++--- apps/frontend/src/components/Navbar/DesktopNavbar.tsx | 2 +- apps/frontend/src/sections/individuals/Hero/index.tsx | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 5681d7f1c..45bc0c956 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -49,6 +49,7 @@ "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "catalog:", "crypto-js": "^4.2.0", "i18next": "^24.2.3", "input-otp": "^1.4.2", diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx index 305c9d645..c4708bcac 100644 --- a/apps/frontend/src/components/Globe/index.tsx +++ b/apps/frontend/src/components/Globe/index.tsx @@ -136,9 +136,14 @@ function useDragRotation(phiRef: React.RefObject, thetaRef: React.RefObj } function subscribeToBreakpoints(callback: () => void) { - const mq = window.matchMedia("(min-width: 1024px), (min-width: 640px)"); - mq.addEventListener("change", callback); - return () => mq.removeEventListener("change", callback); + const mqLg = window.matchMedia("(min-width: 1024px)"); + const mqSm = window.matchMedia("(min-width: 640px)"); + mqLg.addEventListener("change", callback); + mqSm.addEventListener("change", callback); + return () => { + mqLg.removeEventListener("change", callback); + mqSm.removeEventListener("change", callback); + }; } interface GlobeProps { diff --git a/apps/frontend/src/components/Navbar/DesktopNavbar.tsx b/apps/frontend/src/components/Navbar/DesktopNavbar.tsx index 9c4264a26..f9c001ec2 100644 --- a/apps/frontend/src/components/Navbar/DesktopNavbar.tsx +++ b/apps/frontend/src/components/Navbar/DesktopNavbar.tsx @@ -59,7 +59,7 @@ export const DesktopNavbar = () => {
- + Open App
diff --git a/apps/frontend/src/sections/individuals/Hero/index.tsx b/apps/frontend/src/sections/individuals/Hero/index.tsx index 29d7987e4..a915c597e 100644 --- a/apps/frontend/src/sections/individuals/Hero/index.tsx +++ b/apps/frontend/src/sections/individuals/Hero/index.tsx @@ -12,7 +12,7 @@ export const Hero = () => { return (
From 128d6b63c14dec385dce30db7497929172aaaaed Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 1 Apr 2026 18:41:17 +0200 Subject: [PATCH 09/16] address PR comments --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bddd729d3..81149619e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "bcrypt": "5.1.1", "big.js": "^7.0.1", "clsx": "^1.2.1", + "cobe": "^2.0.1", "concurrently": "^9.1.2", "prettier": "^2.8.4", "stellar-sdk": "^13.1.0", @@ -45,7 +46,6 @@ }, "dependencies": { "big.js": "^7.0.1", - "cobe": "^2.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3" From aecd4e8c8940d4af17596f519343af0059bb96ef Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:33:32 +0000 Subject: [PATCH 10/16] fix(security): autofix Express is not emitting security headers --- apps/api/webhooks-cache/index.ts | 2 ++ bun.lock | 6 +++++- package.json | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/webhooks-cache/index.ts b/apps/api/webhooks-cache/index.ts index 021175c06..5ea1fdcf6 100644 --- a/apps/api/webhooks-cache/index.ts +++ b/apps/api/webhooks-cache/index.ts @@ -1,5 +1,6 @@ import bodyParser from "body-parser"; import express, { type NextFunction, type Request, type Response } from "express"; +import helmet from "helmet"; import httpStatus from "http-status"; import logger from "../src/config/logger"; @@ -32,6 +33,7 @@ class EventStore { } const app = express(); +app.use(helmet()); const PORT = process.env.PORT || 3000; const PASSWORD = process.env.PASSWORD || "bananas"; const MAX_EVENTS = 1000; diff --git a/bun.lock b/bun.lock index 09dcae887..09a10062c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "vortex-monorepo", "dependencies": { "big.js": "^7.0.1", + "helmet": "8.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3", @@ -3113,7 +3115,7 @@ "heap": ["heap@0.2.7", "", {}, "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="], - "helmet": ["helmet@4.6.0", "", {}, "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="], + "helmet": ["helmet@8.1.0", "", {}, "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg=="], "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], @@ -5449,6 +5451,8 @@ "vortex-backend/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "vortex-backend/helmet": ["helmet@4.6.0", "", {}, "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="], + "vortex-frontend/numora-react": ["numora-react@3.0.3", "", {}, "sha512-42wqglFsDZNsYUwy09yBS5Kd1U0ZmBiKFpJDmdzAPnXZd4MmZfRA7NyTMO7uTrpQYCTkc5URJ/l56K1f5ISJWg=="], "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], diff --git a/package.json b/package.json index e8fa11f86..1c75aae85 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "big.js": "^7.0.1", + "helmet": "8.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3" From 2ad58638c8025f23f5097d430021e70ff38a1546 Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:33:49 +0000 Subject: [PATCH 11/16] fix(security): autofix 3rd party Github Actions should be pinned --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f498c7fd8..7ad92ef6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 - name: 🧩 Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.1 From d5d3b8a8d8eb7cb4e13098c7d0f4613fb3676940 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 2 Apr 2026 20:35:48 +0200 Subject: [PATCH 12/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ad92ef6a..ee511c08e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: bun-version: 1.3.1 - name: 🧩 Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Check versions run: | From 74df56f99e0b00467d1c42ee62399d6cf523cdc0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 2 Apr 2026 21:45:45 +0200 Subject: [PATCH 13/16] Remove dependency --- bun.lock | 6 +----- package.json | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index 09a10062c..09dcae887 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "vortex-monorepo", "dependencies": { "big.js": "^7.0.1", - "helmet": "8.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3", @@ -3115,7 +3113,7 @@ "heap": ["heap@0.2.7", "", {}, "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="], - "helmet": ["helmet@8.1.0", "", {}, "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg=="], + "helmet": ["helmet@4.6.0", "", {}, "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="], "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], @@ -5451,8 +5449,6 @@ "vortex-backend/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "vortex-backend/helmet": ["helmet@4.6.0", "", {}, "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="], - "vortex-frontend/numora-react": ["numora-react@3.0.3", "", {}, "sha512-42wqglFsDZNsYUwy09yBS5Kd1U0ZmBiKFpJDmdzAPnXZd4MmZfRA7NyTMO7uTrpQYCTkc5URJ/l56K1f5ISJWg=="], "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], diff --git a/package.json b/package.json index 1c75aae85..e8fa11f86 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "dependencies": { "big.js": "^7.0.1", - "helmet": "8.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3" From 6124c07e9274ba696277f29dcf214413338b9d77 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Mon, 6 Apr 2026 12:38:00 -0300 Subject: [PATCH 14/16] disable button verifications --- .../src/components/Avenia/AveniaVerificationForm/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx b/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx index 8f8929836..3402ae8ef 100644 --- a/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx +++ b/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx @@ -27,7 +27,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany }; // formState.isValid is not working as expected, so we need to check the errors - const isFormInvalid = Object.keys(form.formState.errors).length > 0 || !form.formState.isDirty || form.formState.isSubmitting; + const isFormInvalid = Object.keys(form.formState.errors).length > 0 || form.formState.isSubmitting; return ( @@ -35,7 +35,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany animate={{ opacity: 1, scale: 1 }} className="mt-8 mb-4 flex w-full flex-col" initial={{ opacity: 0.8, scale: 0.9 }} - onSubmit={handleSubmit(onSubmit)} + onSubmit={onSubmit} transition={{ duration: 0.3 }} >
From 1b7c183cde4bf5c982750f07a9f9fa8671a20636 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Mon, 6 Apr 2026 13:08:55 -0300 Subject: [PATCH 15/16] add empty callback field --- packages/shared/src/services/brla/brlaApiService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/services/brla/brlaApiService.ts b/packages/shared/src/services/brla/brlaApiService.ts index 85161cc0c..f2c8ba002 100644 --- a/packages/shared/src/services/brla/brlaApiService.ts +++ b/packages/shared/src/services/brla/brlaApiService.ts @@ -357,7 +357,8 @@ export class BrlaApiService { */ public async initiateKybLevel1(subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.KybLevel1WebSdk, "POST", query, undefined); + const payload = { redirectUrl: "" }; + return await this.sendRequest(Endpoint.KybLevel1WebSdk, "POST", query, payload); } /** From 02404a12f6c407c34b6cd7d3a6223fcb0b7cfd2d Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 7 Apr 2026 08:09:28 -0300 Subject: [PATCH 16/16] ammend type --- packages/shared/src/services/brla/mappings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/services/brla/mappings.ts b/packages/shared/src/services/brla/mappings.ts index ea74ab8fb..33cefb73a 100644 --- a/packages/shared/src/services/brla/mappings.ts +++ b/packages/shared/src/services/brla/mappings.ts @@ -170,7 +170,7 @@ export interface EndpointMapping { }; [Endpoint.KybLevel1WebSdk]: { POST: { - body: undefined; + body: { redirectUrl: string | undefined }; response: KybLevel1Response; }; GET: {