diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f498c7fd8..ee511c08e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,12 @@ 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 - name: 🧩 Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Check versions run: | 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/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, 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/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 }} >
diff --git a/apps/frontend/src/components/Globe/index.tsx b/apps/frontend/src/components/Globe/index.tsx new file mode 100644 index 000000000..c4708bcac --- /dev/null +++ b/apps/frontend/src/components/Globe/index.tsx @@ -0,0 +1,215 @@ +import createGlobe from "cobe"; +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"; +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 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: 960, + md: 780, + sm: 560 +} as const; + +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; + +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 = 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, theta: number, size: number) { + const pixelRadius = 0.8 * (size / 2); + 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(theta); + const sinTheta = Math.sin(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 null; + + return { + x: size / 2 + sx * pixelRadius, + y: size / 2 - sy * pixelRadius + }; +} + +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, theta, 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, 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; + 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 = () => { + isDraggingRef.current = false; + }; + + return { isDraggingRef, onPointerCancel: onPointerUp, onPointerDown, onPointerMove, onPointerUp }; +} + +function subscribeToBreakpoints(callback: () => void) { + 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 { + className?: string; +} + +export const Globe = ({ className }: GlobeProps) => { + const reducedMotion = prefersReducedMotion(); + 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 { 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 (!isDraggingRef.current) { + phiRef.current += NORMAL_SPEED; + thetaRef.current += VERTICAL_SPEED; + } + globe.update({ phi: phiRef.current, theta: thetaRef.current }); + updateMarkers(phiRef.current, thetaRef.current, size, markerRefs); + rafId = requestAnimationFrame(tick); + }; + rafId = requestAnimationFrame(tick); + return () => { + globe.destroy(); + cancelAnimationFrame(rafId); + }; + }, [reducedMotion, size]); + + return ( +
+ +
+ {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/components/Navbar/DesktopNavbar.tsx b/apps/frontend/src/components/Navbar/DesktopNavbar.tsx index 8d8816422..f9c001ec2 100644 --- a/apps/frontend/src/components/Navbar/DesktopNavbar.tsx +++ b/apps/frontend/src/components/Navbar/DesktopNavbar.tsx @@ -59,8 +59,8 @@ export const DesktopNavbar = () => {
- - 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 b6bf02bd0..a915c597e 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 = () => { @@ -13,9 +12,9 @@ export const Hero = () => { return (
-
+
{
-
- - -
-
+
diff --git a/bun.lock b/bun.lock index 09dcae887..fc0c10029 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", @@ -2527,6 +2528,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/contracts/relayer/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts b/contracts/relayer/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts index 4c8c1de04..1bb69a5af 100644 --- a/contracts/relayer/typechain-types/@openzeppelin/contracts/interfaces/IERC5267.ts +++ b/contracts/relayer/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/contracts/relayer/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts b/contracts/relayer/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts index 5fe274bab..965ff39e5 100644 --- a/contracts/relayer/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.ts +++ b/contracts/relayer/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/contracts/relayer/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts b/contracts/relayer/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts index 892c84bea..946bec79a 100644 --- a/contracts/relayer/typechain-types/@openzeppelin/contracts/utils/cryptography/EIP712.ts +++ b/contracts/relayer/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/contracts/relayer/typechain-types/contracts/MockERC20Permit.ts b/contracts/relayer/typechain-types/contracts/MockERC20Permit.ts index 454094882..a95156ec0 100644 --- a/contracts/relayer/typechain-types/contracts/MockERC20Permit.ts +++ b/contracts/relayer/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; diff --git a/package.json b/package.json index e8fa11f86..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", 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); } /** 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: {