├── src ├── styles │ ├── _variables.scss │ ├── _mixin.scss │ ├── Layout.module.scss │ ├── _animations.scss │ └── global.scss ├── config │ ├── index.ts │ └── faucet.ts ├── vite-env.d.ts ├── assets │ ├── cardDesign.png │ ├── coinbaseLogo.png │ ├── coins │ │ ├── usdc01.png │ │ ├── usdc02.png │ │ ├── usdc03.png │ │ └── usdc04.png │ ├── myUsdcLogo.png │ ├── myUsdcAltLogo.png │ ├── avatars │ │ ├── avatar01.png │ │ ├── avatar02.png │ │ ├── avatar03.png │ │ ├── avatar04.png │ │ └── avatar05.png │ ├── flags │ │ ├── britainFlagBg.png │ │ ├── canadaFlagBg.png │ │ ├── australiaFlagBg.png │ │ ├── britainFlag.svg │ │ ├── canadaFlag.svg │ │ └── australiaFlag.svg │ ├── onboarding │ │ ├── onboarding01.png │ │ ├── onboarding02.png │ │ └── onboarding03.png │ ├── radioSelectedIcon.svg │ ├── successCheckIcon.svg │ ├── backIcon.svg │ ├── warningIcon.svg │ ├── negativeArrowIcon.svg │ ├── positiveArrowIcon.svg │ ├── profileIcon.svg │ ├── infoIcon.svg │ ├── nextIcon.svg │ ├── copyIcon.svg │ ├── profileActiveIcon.svg │ ├── buyIcon.svg │ ├── walletIcon.svg │ ├── walletActiveIcon.svg │ ├── sendIcon.svg │ ├── profileAltIcon.svg │ ├── amexLogo.svg │ ├── index.ts │ ├── historyIcon.svg │ ├── historyActiveIcon.svg │ └── qrIcon.svg ├── hooks │ ├── useGetUsdRates.ts │ ├── useGetTransfers.ts │ ├── useGetUser.ts │ ├── useTransferAsset.ts │ ├── useFundWallet.ts │ └── useGetRecentContacts.ts ├── pages │ ├── Login │ │ ├── Login.module.scss │ │ └── index.tsx │ ├── Register │ │ ├── Register.module.scss │ │ └── index.tsx │ ├── Wallet │ │ ├── Wallet.module.scss │ │ └── index.tsx │ ├── Splash │ │ ├── Splash.module.scss │ │ └── index.tsx │ ├── Transfers │ │ ├── Transfers.module.scss │ │ └── index.tsx │ ├── Profile │ │ ├── index.tsx │ │ └── Profile.module.scss │ ├── Onboarding │ │ ├── Onboarding.module.scss │ │ └── index.tsx │ └── Send │ │ ├── Send.module.scss │ │ └── index.tsx ├── layouts │ ├── AppLayout.tsx │ └── RootLayout.tsx ├── utils │ ├── web3-avatar.ts │ ├── index.tsx │ └── animations.ts ├── components │ ├── Wallet │ │ ├── Modal │ │ │ ├── Modal.module.scss │ │ │ └── index.tsx │ │ ├── QuickTransfer │ │ │ ├── QuickTransfer.module.scss │ │ │ └── index.tsx │ │ ├── QrModal │ │ │ ├── QrModal.module.scss │ │ │ └── index.tsx │ │ ├── MyUsdc │ │ │ ├── MyUsdc.module.scss │ │ │ └── index.tsx │ │ ├── ExchangeRate │ │ │ ├── ExchangeRate.module.scss │ │ │ └── index.tsx │ │ ├── Card │ │ │ ├── Card.module.scss │ │ │ └── index.tsx │ │ └── BuyModal │ │ │ ├── index.tsx │ │ │ └── BuyModal.module.scss │ ├── BottomNavBar │ │ ├── BottomNavBar.module.scss │ │ └── index.tsx │ ├── Web3Avatar │ │ └── index.tsx │ └── Header │ │ ├── Header.module.scss │ │ └── index.tsx ├── index.css ├── api │ └── index.ts ├── types │ └── api.types.ts ├── main.tsx └── contexts │ └── user.context.tsx ├── public ├── favicon.ico └── vite.svg ├── vercel.json ├── tsconfig.json ├── .env.example ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── README.md └── package.json /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * as faucetConfig from "./faucet"; -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/cardDesign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/cardDesign.png -------------------------------------------------------------------------------- /src/assets/coinbaseLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/coinbaseLogo.png -------------------------------------------------------------------------------- /src/assets/coins/usdc01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/coins/usdc01.png -------------------------------------------------------------------------------- /src/assets/coins/usdc02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/coins/usdc02.png -------------------------------------------------------------------------------- /src/assets/coins/usdc03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/coins/usdc03.png -------------------------------------------------------------------------------- /src/assets/coins/usdc04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/coins/usdc04.png -------------------------------------------------------------------------------- /src/assets/myUsdcLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/myUsdcLogo.png -------------------------------------------------------------------------------- /src/assets/myUsdcAltLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/myUsdcAltLogo.png -------------------------------------------------------------------------------- /src/assets/avatars/avatar01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/avatars/avatar01.png -------------------------------------------------------------------------------- /src/assets/avatars/avatar02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/avatars/avatar02.png -------------------------------------------------------------------------------- /src/assets/avatars/avatar03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/avatars/avatar03.png -------------------------------------------------------------------------------- /src/assets/avatars/avatar04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/avatars/avatar04.png -------------------------------------------------------------------------------- /src/assets/avatars/avatar05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/avatars/avatar05.png -------------------------------------------------------------------------------- /src/assets/flags/britainFlagBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/flags/britainFlagBg.png -------------------------------------------------------------------------------- /src/assets/flags/canadaFlagBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/flags/canadaFlagBg.png -------------------------------------------------------------------------------- /src/assets/flags/australiaFlagBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/flags/australiaFlagBg.png -------------------------------------------------------------------------------- /src/assets/onboarding/onboarding01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/onboarding/onboarding01.png -------------------------------------------------------------------------------- /src/assets/onboarding/onboarding02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/onboarding/onboarding02.png -------------------------------------------------------------------------------- /src/assets/onboarding/onboarding03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeimLabs/coinbase-myusdc-frontend/HEAD/src/assets/onboarding/onboarding03.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_API= 2 | # Clerk Quickstart: https://clerk.com/docs/quickstarts/react 3 | VITE_CLERK_PUBLISHABLE_KEY= -------------------------------------------------------------------------------- /src/assets/radioSelectedIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { nodePolyfills } from 'vite-plugin-node-polyfills' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), nodePolyfills()], 8 | }) 9 | -------------------------------------------------------------------------------- /src/assets/successCheckIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/backIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/styles/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin center($direction: row) { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: $direction; 6 | } 7 | 8 | @mixin hide-scrollbar { 9 | scrollbar-width: none; 10 | -ms-overflow-style: none; 11 | &::-webkit-scrollbar { 12 | display: none; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/styles/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .layoutContainer { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | 6 | .rootLayout { 7 | flex-grow: 1; 8 | position: relative; 9 | width: 100%; 10 | overflow-y: auto; 11 | } 12 | } 13 | 14 | .appLayout { 15 | padding: 0px 20px; 16 | overflow-x: hidden; 17 | } -------------------------------------------------------------------------------- /src/config/faucet.ts: -------------------------------------------------------------------------------- 1 | // Maximum faucet request amount 2 | export const MAX_TOTAL_AMOUNT = 100; 3 | 4 | // Maximum faucet request amount 5 | export const MAX_REQUEST_AMOUNT = 10; 6 | 7 | // Duration (in epochs) between faucet requests 8 | export const MIN_REQUEST_INTERVAL = 3600 // 1 hour 9 | 10 | // Initial amount to fund new wallets with 11 | export const INITIAL_AMOUNT = 1; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My USDC 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/useGetUsdRates.ts: -------------------------------------------------------------------------------- 1 | import { keepPreviousData, useQuery } from "@tanstack/react-query" 2 | import { getUsdRates } from "../api" 3 | 4 | export const useGetUsdRates = () => { 5 | return useQuery({ 6 | queryKey: ["getUsdRates"], 7 | queryFn: async () => getUsdRates(), 8 | placeholderData: keepPreviousData, 9 | refetchOnWindowFocus: false, 10 | refetchInterval: 60000 // 1 minute 11 | }); 12 | } -------------------------------------------------------------------------------- /src/hooks/useGetTransfers.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query" 2 | import { getTransfers } from "../api" 3 | import { useAuth } from "@clerk/clerk-react" 4 | 5 | export const useGetTransfers = () => { 6 | const { getToken } = useAuth(); 7 | 8 | return useQuery({ 9 | queryKey: ["getTransfers"], 10 | queryFn: async () => getTransfers((await getToken()) as string), 11 | refetchOnWindowFocus: false 12 | }) 13 | } -------------------------------------------------------------------------------- /src/assets/warningIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/negativeArrowIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/positiveArrowIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/profileIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/useGetUser.ts: -------------------------------------------------------------------------------- 1 | import { keepPreviousData, useQuery } from "@tanstack/react-query" 2 | import { getUser } from "../api" 3 | import { useAuth } from "@clerk/clerk-react" 4 | 5 | export const useGetUser = () => { 6 | const { getToken, isLoaded, isSignedIn } = useAuth(); 7 | 8 | return useQuery({ 9 | queryKey: ["getUser", isLoaded, isSignedIn], 10 | queryFn: async () => getUser((await getToken()) as string), 11 | placeholderData: keepPreviousData, 12 | refetchOnWindowFocus: false, 13 | enabled: !!isSignedIn 14 | }); 15 | } -------------------------------------------------------------------------------- /src/pages/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | position: relative; 11 | background: linear-gradient(360deg, #000000 0%, #1E1E1E 59.13%), #000000; 12 | overflow: hidden; 13 | padding: 20px; 14 | 15 | .logoContainer { 16 | max-width: 250px; 17 | 18 | .logo { 19 | width: 100%; 20 | height: auto; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/pages/Register/Register.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | position: relative; 11 | background: linear-gradient(360deg, #000000 0%, #1E1E1E 59.13%), #000000; 12 | overflow: hidden; 13 | padding: 20px; 14 | 15 | .logoContainer { 16 | max-width: 250px; 17 | 18 | .logo { 19 | width: 100%; 20 | height: auto; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/Wallet/Wallet.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: start; 7 | justify-content: start; 8 | width: 100%; 9 | gap: 25px; 10 | 11 | .sectionContainer { 12 | display: flex; 13 | align-items: start; 14 | justify-content: center; 15 | flex-direction: column; 16 | width: 100%; 17 | gap: 10px; 18 | 19 | h4 { 20 | font-size: medium; 21 | font-weight: 400; 22 | margin-left: 15px; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/styles/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes shimmer { 2 | 0% { 3 | background-position: -200% 0; 4 | } 5 | 6 | 100% { 7 | background-position: 200% 0; 8 | } 9 | } 10 | 11 | @keyframes vibrate { 12 | 0% { 13 | transform: translateX(0); 14 | } 15 | 16 | 20% { 17 | transform: translateX(-4px); 18 | } 19 | 20 | 40% { 21 | transform: translateX(4px); 22 | } 23 | 24 | 60% { 25 | transform: translateX(-4px); 26 | } 27 | 28 | 80% { 29 | transform: translateX(4px); 30 | } 31 | 32 | 100% { 33 | transform: translateX(0); 34 | } 35 | } -------------------------------------------------------------------------------- /src/assets/infoIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/nextIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from '@clerk/clerk-react'; 2 | import { Outlet, useNavigate } from 'react-router-dom'; 3 | import { useEffect } from 'react'; 4 | import styles from "../styles/Layout.module.scss"; 5 | 6 | export default function AppLayout() { 7 | const { userId, isLoaded } = useAuth() 8 | const navigate = useNavigate() 9 | 10 | useEffect(() => { 11 | if (isLoaded) { 12 | if (!userId) navigate('/login') 13 | else if (userId) navigate('/wallet') 14 | } 15 | }, [isLoaded]) 16 | 17 | if (!isLoaded) return 'Loading...' 18 | 19 | return ( 20 |
21 | 22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /src/utils/web3-avatar.ts: -------------------------------------------------------------------------------- 1 | // Based on `web3-avatar`: https://github.com/JackHamer09/web3-avatar 2 | 3 | export function getGradientColors(address: string) { 4 | const seedArr = address.match(/.{1,7}/g)?.splice(0, 5); 5 | const colors: string[] = []; 6 | 7 | seedArr?.forEach((seed) => { 8 | let hash = 0; 9 | for (let i = 0; i < seed.length; i += 1) { 10 | hash = seed.charCodeAt(i) + ((hash << 5) - hash); 11 | hash = hash & hash; 12 | } 13 | 14 | const rgb = [0, 0, 0]; 15 | for (let i = 0; i < 3; i += 1) { 16 | const value = (hash >> (i * 8)) & 255; 17 | rgb[i] = value; 18 | } 19 | colors.push(`rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`); 20 | }); 21 | 22 | return colors; 23 | } -------------------------------------------------------------------------------- /src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import Web3Avatar from "../components/Web3Avatar" 2 | import { RecentContact } from "../types/api.types" 3 | 4 | export const getImageFromUser = (contact: RecentContact) => { 5 | if (contact.destinationUser) { 6 | if (contact.destinationUser.imageUrl) 7 | return PFP 8 | else 9 | return PFP 10 | } else { 11 | return 12 | } 13 | } 14 | 15 | export const shortAddress = (address: string | undefined) => { 16 | if (address) 17 | return `${address.slice(0, 5)}....${address.slice(-4)}` 18 | } -------------------------------------------------------------------------------- /src/assets/copyIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/profileActiveIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/Wallet/Modal/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/animations"; 2 | 3 | .main { 4 | position: absolute; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100vh; 10 | z-index: 9; 11 | visibility: hidden; 12 | 13 | &.open { 14 | visibility: visible; 15 | } 16 | 17 | .modal { 18 | position: fixed; 19 | bottom: -100%; 20 | z-index: 10; 21 | width: 100%; 22 | min-width: 320px; 23 | max-width: 450px; 24 | height: 50vh; 25 | min-height: 335px; 26 | background-color: black; 27 | padding: 20px; 28 | border-radius: 30px 30px 0px 0px; 29 | transition-duration: 0.3s; 30 | 31 | &.open { 32 | bottom: 0; 33 | transition-duration: 0.3s; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/components/Wallet/QuickTransfer/QuickTransfer.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-evenly; 5 | width: 100%; 6 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 7 | border-radius: 10px; 8 | position: relative; 9 | overflow: hidden; 10 | height: 80px; 11 | padding: 20px 10px; 12 | 13 | .contactContainer { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | overflow: hidden; 18 | border-radius: 100%; 19 | height: 40px; 20 | width: 40px; 21 | 22 | img { 23 | height: 40px; 24 | width: 40px; 25 | cursor: pointer; 26 | } 27 | 28 | .blank { 29 | height: 40px; 30 | width: 40px; 31 | background-color: black; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/assets/buyIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/BottomNavBar/BottomNavBar.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-direction: column; 6 | width: 100%; 7 | position: sticky; 8 | gap: 10px; 9 | bottom: 0px; 10 | background-color: black; 11 | z-index: 2; 12 | 13 | .fadingHr { 14 | width: 95%; 15 | height: 1px; 16 | border: none; 17 | background: linear-gradient(to right, 18 | rgba(255, 255, 255, 0), 19 | rgba(255, 255, 255, 1) 50%, 20 | rgba(255, 255, 255, 0)); 21 | } 22 | 23 | .tabContainer { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-around; 27 | width: 100%; 28 | 29 | .tab { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | font-size: smaller; 35 | gap: 10px; 36 | cursor: pointer; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "https://fonts.googleapis.com/css?family=Poppins:300,400,600,700"; 3 | 4 | * { 5 | box-sizing: border-box; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | html { 11 | background-color: black; 12 | font-family: "Poppins"; 13 | width: 100%; 14 | margin: 0 auto; 15 | color: #fff; 16 | overflow-x: hidden; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | 21 | * { 22 | font-family: "Poppins"; 23 | } 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | flex-direction: column; 30 | align-items: stretch; 31 | justify-content: center; 32 | width: 100%; 33 | min-width: 320px; 34 | max-width: 450px; 35 | min-height: 100vh; 36 | } 37 | 38 | /* Chrome, Safari, Edge, Opera */ 39 | input::-webkit-outer-spin-button, 40 | input::-webkit-inner-spin-button { 41 | -webkit-appearance: none; 42 | margin: 0; 43 | } 44 | 45 | /* Firefox */ 46 | input[type=number] { 47 | -moz-appearance: textfield; 48 | } -------------------------------------------------------------------------------- /src/pages/Wallet/index.tsx: -------------------------------------------------------------------------------- 1 | import Card from "../../components/Wallet/Card"; 2 | import MyUsdc from "../../components/Wallet/MyUsdc"; 3 | import QuickTransfer from "../../components/Wallet/QuickTransfer"; 4 | import ExchangeRate from "../../components/Wallet/ExchangeRate"; 5 | import styles from "./Wallet.module.scss"; 6 | 7 | export default function Wallet() { 8 | return ( 9 |
10 |
11 |

Your Non Digital Cash Balance

12 | 13 |
14 |
15 |

USDC Overview

16 | 17 |
18 |
19 |

Quick Transfer

20 | 21 |
22 |
23 |

Exchange Rate

24 | 25 |
26 |
27 | ); 28 | } -------------------------------------------------------------------------------- /src/components/Wallet/QrModal/QrModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/animations"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-around; 8 | width: 100%; 9 | height: 100%; 10 | 11 | .fadingHr { 12 | width: 95%; 13 | height: 1px; 14 | border: none; 15 | background: linear-gradient(to right, 16 | rgba(255, 255, 255, 0), 17 | rgba(255, 255, 255, 1) 50%, 18 | rgba(255, 255, 255, 0)); 19 | } 20 | 21 | .title { 22 | color: #797979; 23 | font-weight: 600; 24 | font-size: large; 25 | } 26 | 27 | .subtitle { 28 | color: white; 29 | font-weight: 100; 30 | font-size: x-small; 31 | } 32 | 33 | .addressContainer { 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | gap: 5px; 38 | 39 | img { 40 | height: 20px; 41 | width: auto; 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coinbase My USDC | Frontend 2 | 3 | ![React](https://img.shields.io/badge/-React-333333?style=for-the-badge&logo=react&logoColor=61dbfb) 4 | ![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 5 | ![TypeScript](https://img.shields.io/badge/-TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 6 | ![SCSS](https://img.shields.io/badge/-SCSS-cd6799?style=for-the-badge&logo=SASS&logoColor=white) 7 | ![Coinbase](https://img.shields.io/badge/Coinbase-0052FF?style=for-the-badge&logo=Coinbase&logoColor=white) 8 | ![Pnpm](https://img.shields.io/badge/pnpm-yellow?style=for-the-badge&logo=pnpm&logoColor=white) 9 | 10 |
11 | 12 | ## Prerequisites 13 | 14 | - Git 15 | - NodeJs 16 | - pnpm 17 | 18 | ## Getting Started 19 | 20 | - Install dependencies 21 | 22 | ```sh 23 | pnpm i 24 | ``` 25 | 26 | - Fill in the environment variables in the `.env` file, refer to the `.env.example` file for the required variables. 27 | - Make sure [configs](./src/configs/) is updated 28 | 29 | ## Run Dev 30 | 31 | ```sh 32 | pnpm run dev 33 | ``` 34 | 35 | ## Build Prod 36 | 37 | ```sh 38 | pnpm run build 39 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coinbase-myusdc-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@clerk/clerk-react": "^5.7.0", 14 | "@clerk/themes": "^2.1.27", 15 | "@coinbase/coinbase-sdk": "^0.4.0", 16 | "@tanstack/react-query": "^5.55.2", 17 | "axios": "^1.7.7", 18 | "framer-motion": "^11.5.4", 19 | "qrcode.react": "^4.0.1", 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1", 22 | "react-loading-skeleton": "^3.4.0", 23 | "react-router-dom": "^6.26.2", 24 | "react-toastify": "^10.0.5" 25 | }, 26 | "devDependencies": { 27 | "@eslint/js": "^9.9.0", 28 | "@types/react": "^18.3.3", 29 | "@types/react-dom": "^18.3.0", 30 | "@vitejs/plugin-react": "^4.3.1", 31 | "eslint": "^9.9.0", 32 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 33 | "eslint-plugin-react-refresh": "^0.4.9", 34 | "globals": "^15.9.0", 35 | "sass": "^1.78.0", 36 | "typescript": "^5.5.3", 37 | "typescript-eslint": "^8.0.1", 38 | "vite": "^5.4.1", 39 | "vite-plugin-node-polyfills": "^0.22.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useTransferAsset.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query" 2 | import { transferAsset } from "../api" 3 | import { useAuth } from "@clerk/clerk-react" 4 | import { toast } from "react-toastify"; 5 | 6 | export const useTransferAsset = (recipient: string, asset: string, amount: number) => { 7 | const queryClient = useQueryClient(); 8 | const { getToken } = useAuth(); 9 | 10 | const mutation = useMutation({ 11 | mutationFn: async () => 12 | transferAsset((await getToken()) as string, { asset, data: { recipient, amount } }), 13 | onSuccess: async () => { 14 | try { 15 | await queryClient.invalidateQueries({ queryKey: ['getUser'] }) 16 | } catch (err) { 17 | console.error(err); 18 | } 19 | } 20 | }); 21 | 22 | const _transferAsset = () => { 23 | const _promise = mutation.mutateAsync(); 24 | toast.promise(_promise, { 25 | pending: "Sending...", 26 | success: "Transfer successful!", 27 | error: "Transfer failed, please check history or try again!" 28 | }) 29 | } 30 | 31 | return { 32 | transferAsset: _transferAsset, 33 | ...mutation 34 | } 35 | } -------------------------------------------------------------------------------- /src/assets/walletIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/clerk-react"; 2 | import { dark } from "@clerk/themes"; 3 | import { myUsdcLogo } from "../../assets"; 4 | import styles from "./Login.module.scss"; 5 | 6 | export default function Login() { 7 | return ( 8 |
9 |
10 | MyUSDC 11 |
12 | 33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /src/pages/Register/index.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/clerk-react"; 2 | import { dark } from "@clerk/themes"; 3 | import { myUsdcLogo } from "../../assets"; 4 | import styles from "./Register.module.scss"; 5 | 6 | export default function Register() { 7 | return ( 8 |
9 |
10 | MyUSDC 11 |
12 | 33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /src/utils/animations.ts: -------------------------------------------------------------------------------- 1 | export const floatingAnimation = { 2 | float: { 3 | y: [0, -10, 0], // Vertical floating animation 4 | rotate: [0, 10, -10, 0], // Rotation animation 5 | transition: { 6 | duration: 4, // Duration of the animation 7 | ease: "easeInOut", // Easing function 8 | repeat: Infinity, // Loop the animation infinitely 9 | repeatType: "loop" as const, // Restart from the beginning of the animation 10 | }, 11 | }, 12 | }; 13 | 14 | export const glowAnimation = { 15 | glow: { 16 | filter: [ 17 | 'drop-shadow(0 0 5px rgba(0, 255, 255, 0.1))', // Initial glow 18 | 'drop-shadow(0 0 10px rgba(0, 255, 255, 0.15))', // Stronger glow 19 | 'drop-shadow(0 0 15px rgba(0, 255, 255, 0.2))', // Strongest glow 20 | 'drop-shadow(0 0 10px rgba(0, 255, 255, 0.15))', // Reverse to stronger 21 | 'drop-shadow(0 0 5px rgba(0, 255, 255, 0.1))' // Back to initial light glow 22 | ], 23 | transition: { 24 | duration: 2, // Time for one pulse cycle 25 | ease: 'easeInOut', 26 | repeat: Infinity, // Loop indefinitely 27 | repeatType: 'mirror' as const, // Smooth transition between keyframes 28 | }, 29 | }, 30 | }; -------------------------------------------------------------------------------- /src/components/Web3Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { getGradientColors } from "../../utils/web3-avatar"; 3 | 4 | interface Web3AvatarProps { 5 | address: string; 6 | } 7 | 8 | // Based on `web3-avatar`: https://github.com/JackHamer09/web3-avatar 9 | const Web3Avatar: React.FC = ({ address }) => { 10 | const colors = useMemo(() => getGradientColors(address), [address]); 11 | 12 | const avatarStyle = { 13 | "--color-av-1": colors[0], 14 | "--color-av-2": colors[1], 15 | "--color-av-3": colors[2], 16 | "--color-av-4": colors[3], 17 | "--color-av-5": colors[4], 18 | borderRadius: "50%", 19 | boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.1)", 20 | backgroundColor: "var(--color-av-1)", 21 | backgroundImage: ` 22 | radial-gradient(at 66% 77%, var(--color-av-2) 0px, transparent 50%), 23 | radial-gradient(at 29% 97%, var(--color-av-3) 0px, transparent 50%), 24 | radial-gradient(at 99% 86%, var(--color-av-4) 0px, transparent 50%), 25 | radial-gradient(at 29% 88%, var(--color-av-5) 0px, transparent 50%)`, 26 | width: "100px", // Adjust as needed 27 | height: "100px", // Adjust as needed, 28 | cursor: "pointer" 29 | } as React.CSSProperties; 30 | 31 | return
; 32 | }; 33 | 34 | export default Web3Avatar; 35 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/Splash/Splash.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100%; 8 | position: relative; 9 | z-index: 20; 10 | 11 | visibility: visible; 12 | opacity: 1; 13 | transition-duration: 0.6s; 14 | 15 | &.close { 16 | visibility: hidden; 17 | opacity: 0; 18 | transition-duration: 0.6s; 19 | } 20 | 21 | .stepContainer { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | height: 100vh; 26 | max-width: 450px; 27 | transition-duration: 0.3s; 28 | background: linear-gradient(360deg, #000000 0%, #1E1E1E 59.13%), #000000; 29 | overflow: hidden; 30 | position: fixed; 31 | top: 0px; 32 | width: 100%; 33 | } 34 | 35 | .logo { 36 | width: 100%; 37 | height: auto; 38 | padding: 20px; 39 | } 40 | 41 | .float { 42 | position: absolute; 43 | 44 | &.usdc01 { 45 | top: 0; 46 | right: -70px; 47 | } 48 | 49 | &.usdc02 { 50 | top: 15%; 51 | left: -30px; 52 | } 53 | 54 | &.usdc03 { 55 | bottom: 25%; 56 | right: 0px; 57 | } 58 | 59 | &.usdc04 { 60 | bottom: -70px; 61 | left: -70px; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/assets/flags/britainFlag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/flags/canadaFlag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/walletActiveIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | FundWalletRequest, 4 | GetRecentContactsResponse, 5 | GetTransfersResponse, 6 | GetUsdRatesResponse, 7 | GetUserResponse, 8 | TransferAssetRequest, 9 | TransferAssetResponse 10 | } from "../types/api.types"; 11 | 12 | const api = axios.create({ 13 | baseURL: import.meta.env.VITE_BACKEND_API || "http://localhost:5000", 14 | }); 15 | 16 | const getUser = async (token: string) => 17 | api.get("/wallet/user", 18 | { headers: { Authorization: `Bearer ${token}` } }); 19 | 20 | const transferAsset = async (token: string, data: TransferAssetRequest) => 21 | api.post("/wallet/transfer-asset", data, { headers: { Authorization: `Bearer ${token}` } }) 22 | 23 | const fundWallet = async (token: string, data: FundWalletRequest) => 24 | api.post("/wallet/fund", data, { headers: { Authorization: `Bearer ${token}` } }) 25 | 26 | const getTransfers = async (token: string) => 27 | api.get("/wallet/transfers", 28 | { headers: { Authorization: `Bearer ${token}` } }); 29 | 30 | const getRecentContacts = async (token: string) => 31 | api.get("/wallet/recent-contacts", 32 | { headers: { Authorization: `Bearer ${token}` } }); 33 | 34 | const getUsdRates = async () => 35 | axios.get("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json"); 36 | 37 | export { api, getUser, transferAsset, fundWallet, getTransfers, getRecentContacts, getUsdRates }; 38 | -------------------------------------------------------------------------------- /src/layouts/RootLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, useNavigate } from 'react-router-dom'; 2 | import { ClerkProvider, SignedIn } from '@clerk/clerk-react'; 3 | import { UserProvider } from '../contexts/user.context.tsx'; 4 | import BottomNavBar from '../components/BottomNavBar'; 5 | import Header from '../components/Header'; 6 | import styles from "../styles/Layout.module.scss"; 7 | import Splash from '../pages/Splash/index.tsx'; 8 | 9 | const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; 10 | 11 | if (!PUBLISHABLE_KEY) { 12 | throw new Error('Missing Publishable Key'); 13 | } 14 | 15 | export default function RootLayout() { 16 | const navigate = useNavigate(); 17 | 18 | return ( 19 | navigate(to)} 21 | routerReplace={(to) => navigate(to, { replace: true })} 22 | publishableKey={PUBLISHABLE_KEY} 23 | > 24 | 25 |
26 | {/* HEADER HERE */} 27 | 28 |
29 | 30 | 31 | {/* MAIN CONTENT */} 32 |
33 | 34 |
35 | 36 | {/* BOTTOM NAV HERE */} 37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 | ) 45 | } -------------------------------------------------------------------------------- /src/components/Wallet/QrModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | import { copyIcon } from "../../../assets"; 3 | import styles from "./QrModal.module.scss"; 4 | import Modal from "../Modal"; 5 | import { QRCodeSVG } from 'qrcode.react'; 6 | import { useAppUser } from "../../../contexts/user.context"; 7 | import { toast } from "react-toastify"; 8 | import { shortAddress } from "../../../utils"; 9 | 10 | export default function QrModal({ isOpen, setOpen }: { isOpen: boolean, setOpen: Dispatch> }) { 11 | const { user } = useAppUser(); 12 | 13 | const handleCopy = async () => { 14 | if (user?.wallet.address) { 15 | await navigator.clipboard.writeText(user?.wallet.address); 16 | toast.success("Address copied to clipboard!"); 17 | } 18 | } 19 | 20 | return ( 21 | 22 |
23 | {/* TITLE */} 24 | Receive USDC 25 | {/* SUBTITLE */} 26 | Scan to send USDC on Base Sepolia 27 | {/* QR CODE */} 28 | 29 | {/* ADDRESS */} 30 |
31 | Your Address: {shortAddress(user?.wallet.address) || "NA"} 32 | Copy 33 |
34 |
35 |
36 | ); 37 | } -------------------------------------------------------------------------------- /src/assets/sendIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/types/api.types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | userId: string; 3 | name: string; 4 | email: string; 5 | imageUrl: string; 6 | wallet: { 7 | usdBalance: number; 8 | usdcBalance: number | null; 9 | id: string; 10 | address: string; 11 | rewards: { 12 | amount: number; 13 | lastUpdated: Date; 14 | }; 15 | }, 16 | faucet: { 17 | amount: number, 18 | lastRequested: Date 19 | } 20 | } 21 | 22 | export type DestinationUser = { 23 | name: string; 24 | email: string | undefined; 25 | imageUrl: string; 26 | wallet: { 27 | address: string 28 | } 29 | } 30 | 31 | export type GetUserResponse = User; 32 | 33 | export type TransferAssetRequest = { 34 | asset: string, 35 | data: { 36 | recipient: string; 37 | amount: number; 38 | } 39 | } 40 | 41 | export type TransferAssetResponse = { 42 | transactionLink: string, 43 | status: string 44 | } 45 | 46 | 47 | export type FundWalletRequest = { 48 | asset: string, 49 | amount: number 50 | } 51 | 52 | export type Transfer = { 53 | id: string; 54 | destinationAddress: string; 55 | destinationUser: User | null; 56 | assetId: string; 57 | amount: number; 58 | transactionLink: string | undefined; 59 | status: string; 60 | } 61 | 62 | export type GetTransfersResponse = { 63 | transfers: Transfer[] 64 | } 65 | 66 | export type RecentContact = { 67 | destinationAddress: string, 68 | destinationUser: User | DestinationUser | null 69 | } 70 | 71 | export type GetRecentContactsResponse = { 72 | recentContacts: RecentContact[] 73 | } 74 | 75 | export type GetUsdRatesResponse = { 76 | date: string, 77 | usd: { 78 | cad: number, 79 | aud: number, 80 | gbp: number 81 | } 82 | } -------------------------------------------------------------------------------- /src/components/Wallet/MyUsdc/MyUsdc.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | width: 100%; 6 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 7 | border-radius: 20px; 8 | position: relative; 9 | overflow: hidden; 10 | 11 | .vr { 12 | height: 100%; 13 | width: 1px; 14 | background-color: black; 15 | height: 78px; 16 | } 17 | 18 | .balanceContainer { 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: start; 23 | justify-content: center; 24 | padding: 0px 20px; 25 | 26 | .title { 27 | color: #AAAAAA; 28 | font-size: small; 29 | } 30 | 31 | span { 32 | font-size: larger; 33 | display: flex; 34 | align-items: center; 35 | justify-content: start; 36 | min-width: 70px; 37 | } 38 | 39 | .rewardsContainer { 40 | display: flex; 41 | align-items: center; 42 | gap: 10px; 43 | 44 | img { 45 | height: 16px; 46 | width: auto; 47 | cursor: pointer; 48 | } 49 | } 50 | } 51 | 52 | .overlay { 53 | position: absolute; 54 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 55 | font-size: x-small; 56 | text-align: center; 57 | padding: 20px; 58 | visibility: hidden; 59 | opacity: 0; 60 | transition-duration: 0.3s; 61 | cursor: pointer; 62 | 63 | &.open { 64 | opacity: 1; 65 | visibility: visible; 66 | transition-duration: 0.3s; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | position: sticky; 3 | top: 0px; 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | padding: 20px 20px 10px 20px; 9 | background-color: black; 10 | z-index: 2; 11 | 12 | .walletHeader { 13 | width: 100%; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | 18 | .leftContainer { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | gap: 10px; 23 | 24 | .logo { 25 | height: 45px; 26 | width: auto; 27 | } 28 | 29 | .buildContainer { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | height: 30px; 34 | gap: 5px; 35 | background-color: lighten(black, 10%); 36 | padding: 5px 10px; 37 | border-radius: 10px; 38 | cursor: pointer; 39 | 40 | img { 41 | height: 14px; 42 | width: auto; 43 | } 44 | 45 | span { 46 | color: #0051FD; 47 | font-weight: 100; 48 | } 49 | } 50 | } 51 | 52 | 53 | .profile { 54 | height: 40px; 55 | width: auto; 56 | cursor: pointer; 57 | } 58 | } 59 | 60 | .crumbHeader { 61 | width: 100%; 62 | display: flex; 63 | align-items: center; 64 | justify-content: flex-start; 65 | gap: 10px; 66 | 67 | .back { 68 | height: 25px; 69 | width: auto; 70 | cursor: pointer; 71 | } 72 | 73 | span { 74 | text-transform: capitalize; 75 | font-size: large; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/components/Wallet/ExchangeRate/ExchangeRate.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 8 | border-radius: 10px; 9 | position: relative; 10 | overflow: hidden; 11 | 12 | .hr { 13 | background-color: black; 14 | height: 2px; 15 | width: 100%; 16 | } 17 | 18 | .exchangeContainer { 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | padding: 15px 20px; 23 | width: 100%; 24 | background-size: cover; 25 | 26 | .countryContainer { 27 | display: flex; 28 | flex-direction: column; 29 | align-items: start; 30 | justify-content: start; 31 | gap: 10px; 32 | 33 | img { 34 | height: 25px; 35 | width: auto; 36 | } 37 | 38 | span { 39 | font-size: small; 40 | } 41 | 42 | } 43 | 44 | .rateContainer { 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | gap: 30px; 49 | 50 | .change { 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | gap: 5px; 55 | 56 | &.changeUp { 57 | color: #20C505; 58 | } 59 | 60 | &.changeDown { 61 | color: red; 62 | } 63 | 64 | img { 65 | height: 10px; 66 | width: auto; 67 | } 68 | } 69 | 70 | @media screen and (max-width: 355px) { 71 | flex-direction: column; 72 | gap: 5px; 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './styles/global.scss' 4 | 5 | import { RouterProvider, createBrowserRouter } from 'react-router-dom' 6 | 7 | import RootLayout from './layouts/RootLayout.tsx' 8 | import AppLayout from './layouts/AppLayout.tsx' 9 | 10 | import Onboarding from './pages/Onboarding/index.tsx' 11 | import Login from './pages/Login' 12 | import Register from './pages/Register' 13 | import Wallet from './pages/Wallet' 14 | import Transfers from './pages/Transfers' 15 | import Profile from './pages/Profile' 16 | import Send from './pages/Send' 17 | 18 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 19 | 20 | import { ToastContainer } from 'react-toastify'; 21 | import 'react-toastify/dist/ReactToastify.css'; 22 | import { SkeletonTheme } from 'react-loading-skeleton'; 23 | import 'react-loading-skeleton/dist/skeleton.css' 24 | 25 | const queryClient = new QueryClient() 26 | 27 | const router = createBrowserRouter([ 28 | { 29 | element: , 30 | children: [ 31 | { path: '/', element: }, 32 | { path: '/login', element: }, 33 | { path: '/register', element: }, 34 | { 35 | element: , 36 | path: 'wallet', 37 | children: [ 38 | { path: '/wallet/', element: }, 39 | { path: '/wallet/transfers', element: }, 40 | { path: '/wallet/profile', element: }, 41 | { path: '/wallet/send', element: }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | ]) 47 | 48 | createRoot(document.getElementById('root')!).render( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | , 57 | ) 58 | -------------------------------------------------------------------------------- /src/components/Wallet/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; 2 | import styles from "./Modal.module.scss"; 3 | import { useAppUser } from "../../../contexts/user.context"; 4 | 5 | type ModalProps = { 6 | isOpen: boolean; 7 | setOpen: Dispatch>; 8 | children: React.ReactNode; // Accepting children as a prop 9 | } 10 | 11 | export default function Modal({ isOpen, setOpen, children }: ModalProps) { 12 | const modalRef = useRef(null); 13 | const { cardBottom } = useAppUser(); 14 | const [modalHeight, setModalHeight] = useState("50vh") 15 | 16 | // Handle Modal Close 17 | useEffect(() => { 18 | const handleClickOutside = (event: MouseEvent) => { 19 | if (modalRef.current && !modalRef.current.contains(event.target as Node)) { 20 | setOpen(!isOpen); 21 | } 22 | }; 23 | 24 | if (isOpen) { 25 | document.addEventListener('mousedown', handleClickOutside); 26 | } 27 | 28 | return () => { 29 | document.removeEventListener('mousedown', handleClickOutside); 30 | }; 31 | }, [isOpen, setOpen]); 32 | 33 | // Calculate modal height 34 | useEffect(() => { 35 | const calculateModalHeight = () => { 36 | if (cardBottom) { 37 | const _modalHeight = window.innerHeight - cardBottom - 20; 38 | setModalHeight(`${_modalHeight}px`) 39 | } 40 | } 41 | 42 | calculateModalHeight(); 43 | 44 | window.addEventListener('resize', calculateModalHeight); 45 | 46 | return () => window.removeEventListener('resize', calculateModalHeight); 47 | }, [cardBottom]) 48 | 49 | return ( 50 |
51 |
52 | {children} 53 |
54 |
55 | ); 56 | } -------------------------------------------------------------------------------- /src/contexts/user.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction, useContext, useState } from "react"; 2 | import { useGetUser } from "../hooks/useGetUser"; 3 | import { User } from "../types/api.types"; 4 | import { useAuth, useUser } from "@clerk/clerk-react"; 5 | import { useQueryClient } from "@tanstack/react-query"; 6 | 7 | type UserContextType = { 8 | user: User | undefined, 9 | clerkUser: ReturnType | undefined 10 | isUserLoading: boolean | undefined, 11 | signOut: (() => void) | undefined 12 | hasAnimated: boolean | undefined, 13 | setHasAnimated: Dispatch> | undefined, 14 | cardBottom: number | undefined, 15 | setCardBottom: Dispatch> | undefined 16 | } 17 | 18 | const UserContext = createContext({ 19 | user: undefined, 20 | clerkUser: undefined, 21 | isUserLoading: undefined, 22 | signOut: undefined, 23 | hasAnimated: undefined, 24 | setHasAnimated: undefined, 25 | cardBottom: undefined, 26 | setCardBottom: undefined 27 | }); 28 | 29 | export const UserProvider = ({ children }: { children: React.ReactNode }) => { 30 | const queryClient = useQueryClient(); 31 | const getUser = useGetUser(); 32 | const { signOut: clerkSignOut } = useAuth(); 33 | const clerkUser = useUser(); 34 | 35 | const [hasAnimated, setHasAnimated] = useState(false); 36 | const [cardBottom, setCardBottom] = useState(); 37 | 38 | const signOut = async () => { 39 | await clerkSignOut(); 40 | await queryClient.resetQueries({ queryKey: ['getUser'] }); 41 | } 42 | 43 | return 53 | {children} 54 | ; 55 | }; 56 | 57 | export const useAppUser = () => useContext(UserContext); -------------------------------------------------------------------------------- /src/pages/Splash/index.tsx: -------------------------------------------------------------------------------- 1 | import { myUsdcLogo, usdc01, usdc02, usdc03, usdc04 } from "../../assets"; 2 | import styles from "./Splash.module.scss"; 3 | import { useEffect, useState } from "react"; 4 | import { motion } from "framer-motion" 5 | import { floatingAnimation, glowAnimation } from "../../utils/animations"; 6 | 7 | export default function Splash() { 8 | const [minWaitCompleted, setMinWaitCompleted] = useState(false); 9 | 10 | useEffect(() => { 11 | const MIN_WAIT_DURATION = 2000; 12 | 13 | const timeoutId = setTimeout(() => { 14 | setMinWaitCompleted(true); 15 | }, MIN_WAIT_DURATION); 16 | 17 | return () => { 18 | if (timeoutId) { 19 | clearTimeout(timeoutId); 20 | } 21 | }; 22 | }, []); 23 | 24 | return ( 25 |
26 |
27 | 31 | 35 | 39 | 43 | 47 |
48 |
49 | ); 50 | } -------------------------------------------------------------------------------- /src/hooks/useFundWallet.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from "@tanstack/react-query" 2 | import { fundWallet } from "../api" 3 | import { useAuth } from "@clerk/clerk-react" 4 | import { toast } from "react-toastify"; 5 | import { useAppUser } from "../contexts/user.context"; 6 | import { faucetConfig } from "../config"; 7 | 8 | export const useFundWallet = (asset: string, amount: number) => { 9 | const queryClient = useQueryClient(); 10 | const { getToken } = useAuth(); 11 | const { user } = useAppUser(); 12 | 13 | const mutation = useMutation({ 14 | mutationFn: async () => 15 | fundWallet((await getToken()) as string, { asset, amount }), 16 | onSuccess: async () => { 17 | try { 18 | await queryClient.invalidateQueries({ queryKey: ['getUser'] }) 19 | } catch (err) { 20 | console.error(err); 21 | } 22 | } 23 | }); 24 | 25 | const _fundWallet = () => { 26 | if (!user) return; 27 | if (amount > faucetConfig.MAX_REQUEST_AMOUNT) 28 | return toast.error("Requested amount too high"); 29 | if ((user.faucet.amount + amount) > faucetConfig.MAX_TOTAL_AMOUNT) 30 | return toast.error("Purchase limit reached"); 31 | if ((user.wallet.usdBalance - amount) <= 0) 32 | return toast.error("Insufficient balance"); 33 | if (user.faucet.lastRequested) { 34 | const now = new Date(); 35 | const timeSinceLastRequest = (now.getTime() - (new Date(user.faucet.lastRequested))?.getTime()) / 1000; 36 | if (timeSinceLastRequest < faucetConfig.MIN_REQUEST_INTERVAL) 37 | return toast.error("Too many requests, try again later!"); 38 | } 39 | 40 | const _promise = mutation.mutateAsync(); 41 | toast.promise(_promise, { 42 | pending: "Purchasing...", 43 | success: "Purchase successful!", 44 | error: "Purchase failed, please try again!" 45 | }) 46 | } 47 | 48 | return { 49 | fundWallet: _fundWallet, 50 | ...mutation 51 | } 52 | } -------------------------------------------------------------------------------- /src/pages/Transfers/Transfers.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | margin-top: 20px; 5 | display: flex; 6 | align-items: start; 7 | justify-content: center; 8 | width: 100%; 9 | 10 | .monthContainer { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | width: 100%; 16 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 17 | padding: 20px; 18 | gap: 30px; 19 | border-radius: 10px; 20 | 21 | .transferRow { 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | width: 100%; 26 | 27 | .userDetails { 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | gap: 20px; 32 | 33 | .pfp { 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | height: 40px; 38 | width: 40px; 39 | overflow: hidden; 40 | border-radius: 100%; 41 | 42 | img { 43 | height: 40px; 44 | width: auto; 45 | } 46 | } 47 | 48 | .contentContainer { 49 | display: flex; 50 | flex-direction: column; 51 | align-items: start; 52 | justify-content: center; 53 | 54 | .title { 55 | font-weight: bold; 56 | } 57 | 58 | .subtitle { 59 | font-size: small; 60 | color: #AAAAAA; 61 | } 62 | } 63 | } 64 | 65 | .amount { 66 | color: #FF4B55; 67 | font-size: small; 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/components/Wallet/QuickTransfer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import styles from "./QuickTransfer.module.scss"; 3 | import { useGetRecentContacts } from "../../../hooks/useGetRecentContacts"; 4 | import { getImageFromUser } from "../../../utils"; 5 | import { useNavigate } from "react-router-dom"; 6 | import Skeleton from "react-loading-skeleton"; 7 | 8 | export default function QuickTransfer() { 9 | const navigate = useNavigate(); 10 | const { data, isFetching } = useGetRecentContacts(); 11 | 12 | const blanks = useMemo(() => { 13 | const recentContacts = data?.data?.recentContacts; 14 | const blankCount = recentContacts == undefined ? 5 : 5 - (recentContacts?.length || 0); 15 | return Array.from({ length: blankCount }, (_, index) => ( 16 |
17 | {isFetching 18 | ? 19 | :
20 | } 21 |
22 | )); 23 | }, [data?.data, isFetching]); 24 | 25 | return ( 26 |
27 | {data?.data.recentContacts.map((contact, index) => { 28 | return ( 29 |
{ 32 | const searchParams = new URLSearchParams(); 33 | searchParams.append("dest", 34 | contact.destinationUser?.email 35 | ? contact.destinationUser.email 36 | : contact.destinationAddress 37 | ); 38 | navigate({ pathname: `/wallet/send`, search: searchParams.toString() }); 39 | }} 40 | className={styles.contactContainer}> 41 | {getImageFromUser(contact)} 42 |
43 | ) 44 | })} 45 | {blanks} 46 |
47 | ); 48 | } -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from "react-router-dom"; 2 | import styles from "./Header.module.scss"; 3 | import { useEffect, useState } from "react"; 4 | import { backIcon, coinbaseLogo, myUsdcAltLogo, profileAltIcon } from "../../assets"; 5 | 6 | export default function Header() { 7 | const navigate = useNavigate(); 8 | const { pathname } = useLocation(); 9 | const [activeTab, setActiveTab] = useState('wallet'); 10 | 11 | useEffect(() => { 12 | if (pathname.includes("transfers")) 13 | setActiveTab('transfers'); 14 | else if (pathname.includes("profile")) 15 | setActiveTab('profile'); 16 | else if (pathname.includes("send")) 17 | setActiveTab('send'); 18 | else 19 | setActiveTab('wallet'); 20 | }, [pathname]); 21 | 22 | const handleBack = () => { 23 | window.history.back(); 24 | } 25 | 26 | return ( 27 |
28 | { 29 | activeTab == "wallet" 30 | ?
31 |
32 | My USDC 33 |
window.open("https://app.deform.cc/form/30138814-ece7-4a5d-bd30-305b4a687a6f", "__blank")}> 34 | Coinbase 35 | Build with us 36 |
37 |
38 | navigate('/wallet/profile')} className={styles.profile} src={profileAltIcon} alt="Profile" /> 39 |
40 | :
41 | Back 42 | {pathname.split('/').reverse()[0]} 43 |
44 | } 45 |
46 | ); 47 | } -------------------------------------------------------------------------------- /src/components/Wallet/MyUsdc/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { infoIcon } from "../../../assets"; 3 | import { useAppUser } from "../../../contexts/user.context"; 4 | import styles from "./MyUsdc.module.scss"; 5 | import Skeleton from "react-loading-skeleton"; 6 | 7 | export default function MyUsdc() { 8 | const { user, isUserLoading } = useAppUser(); 9 | 10 | const [isOpen, setIsOpen] = useState(false); 11 | 12 | return ( 13 |
14 |
15 | USDC Balance 16 | {isUserLoading 17 | ? $ 18 | : 19 | ${user?.wallet?.usdcBalance?.toLocaleString(undefined, 20 | { maximumFractionDigits: 3, minimumFractionDigits: 2 }) || "NA"} 21 | } 22 | 23 |
24 |
25 |
26 | Rewards 27 |
28 | {isUserLoading 29 | ? $ 30 | : 31 | ${user?.wallet?.rewards?.amount?.toLocaleString(undefined, 32 | { maximumFractionDigits: 3, minimumFractionDigits: 2 }) || "NA"} 33 | } 34 | setIsOpen(true)} src={infoIcon} alt="Info" /> 35 |
36 |
37 |
setIsOpen(false)} className={`${styles.overlay} ${isOpen ? styles.open : ""}`}> 38 | USDC is the world's digital dollar that's fully backed 1-to-1 by 39 | real US dollars. Start earning 3% USDC rewards, or send 40 | USDC to anyone in the world at zero cost. 41 |
42 |
43 | ); 44 | } -------------------------------------------------------------------------------- /src/components/BottomNavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from "react-router-dom"; 2 | import styles from "./BottomNavBar.module.scss"; 3 | import { useEffect, useState } from "react"; 4 | import { historyActiveIcon, historyIcon, profileActiveIcon, profileIcon, walletActiveIcon, walletIcon } from "../../assets"; 5 | 6 | export default function BottomNavBar() { 7 | const navigate = useNavigate(); 8 | const { pathname } = useLocation(); 9 | const [activeTab, setActiveTab] = useState('wallet'); 10 | 11 | useEffect(() => { 12 | if (pathname.includes("transfers")) 13 | setActiveTab('transfers'); 14 | else if (pathname.includes("profile")) 15 | setActiveTab('profile'); 16 | else 17 | setActiveTab('wallet'); 18 | }, [pathname]); 19 | 20 | const handleTabChange = (tab: string) => { 21 | navigate("/wallet/" + tab); 22 | } 23 | 24 | return ( 25 |
26 |
27 |
28 | {/* HISTORY */} 29 |
handleTabChange("transfers")} className={`${styles.tab} ${activeTab == "transfers" ? styles.active : ""}`}> 30 | History 31 | Transfers 32 |
33 | {/* WALLET */} 34 |
handleTabChange("")} className={`${styles.tab} ${activeTab == "wallet" ? styles.active : ""}`}> 35 | Wallet 36 | Wallet 37 |
38 | {/* Profile */} 39 |
handleTabChange("profile")} className={`${styles.tab} ${activeTab == "profile" ? styles.active : ""}`}> 40 | Profile 41 | Profile 42 |
43 |
44 |
45 | ); 46 | } -------------------------------------------------------------------------------- /src/assets/profileAltIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/pages/Profile/index.tsx: -------------------------------------------------------------------------------- 1 | import { qrIcon, walletIcon } from "../../assets"; 2 | import { useAppUser } from "../../contexts/user.context"; 3 | import { shortAddress } from "../../utils"; 4 | import styles from "./Profile.module.scss"; 5 | 6 | export default function Profile() { 7 | const { user, signOut } = useAppUser(); 8 | 9 | return ( 10 |
11 | {/* USER DETAILS */} 12 |
13 |
14 | {user?.name} 15 | {user?.email} 16 | {shortAddress(user?.wallet.address)} 17 |
18 |
19 | PFP 20 | QR 21 |
22 |
23 | {/* ACTIONS */} 24 |
25 |
26 |
27 | Wallet 28 |
29 | ${user?.wallet.usdcBalance} 30 | USDC Balance 31 |
32 |
33 |
34 | 37 |
38 |
39 |
40 | 41 |
42 | MY USDC APP 43 | Version 1.1.0 44 |
45 |
46 | ); 47 | } -------------------------------------------------------------------------------- /src/pages/Onboarding/Onboarding.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100%; 8 | position: relative; 9 | background: linear-gradient(360deg, #000000 0%, #1E1E1E 59.13%), #000000; 10 | overflow: hidden; 11 | z-index: 10; 12 | height: 100vh; 13 | 14 | .stepContainer { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | height: 100vh; 19 | transform: translateX(-110%); 20 | transition-duration: 0.3s; 21 | position: absolute; 22 | top: 0px; 23 | width: 100%; 24 | 25 | &.active { 26 | transform: translateX(0); 27 | transition-duration: 0.3s; 28 | } 29 | 30 | &.onboarding { 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: center; 34 | padding: 30px; 35 | text-align: center; 36 | gap: 30px; 37 | 38 | .onboardingImage { 39 | margin-top: -120px; 40 | width: 140%; 41 | height: auto; 42 | } 43 | 44 | .onboardingText { 45 | margin-top: -100px; 46 | font-size: x-large; 47 | font-weight: 100; 48 | } 49 | 50 | .actionContainer { 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | gap: 5px; 55 | cursor: pointer; 56 | 57 | span { 58 | color: #6786E7; 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | .logo { 66 | width: 100%; 67 | height: auto; 68 | padding: 20px; 69 | } 70 | 71 | .float { 72 | position: absolute; 73 | 74 | &.usdc01 { 75 | top: 0; 76 | right: -70px; 77 | } 78 | 79 | &.usdc02 { 80 | top: 15%; 81 | left: -30px; 82 | } 83 | 84 | &.usdc03 { 85 | bottom: 25%; 86 | right: 0px; 87 | } 88 | 89 | &.usdc04 { 90 | bottom: -70px; 91 | left: -70px; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/assets/amexLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/Transfers/index.tsx: -------------------------------------------------------------------------------- 1 | import Skeleton from "react-loading-skeleton"; 2 | import { useGetTransfers } from "../../hooks/useGetTransfers"; 3 | import { getImageFromUser, shortAddress } from "../../utils"; 4 | import styles from "./Transfers.module.scss"; 5 | 6 | export default function Transfers() { 7 | const { data, isFetching } = useGetTransfers(); 8 | 9 | const SkeletonTransfer = () => { 10 | return ( 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | ) 22 | } 23 | 24 | const skeletonArray = Array.from({ length: 5 }, (_, index) => ); 25 | 26 | return ( 27 |
28 |
29 | {isFetching 30 | ? skeletonArray 31 | : data?.data.transfers.map((transfer, index) => { 32 | return ( 33 |
34 |
35 |
{getImageFromUser(transfer)}
36 |
37 | {transfer.destinationUser 38 | ? {transfer.destinationUser.name} 39 | : {shortAddress(transfer.destinationAddress)}} 40 | Status: {transfer.status} 41 |
42 |
43 | - {transfer.amount} USDC 44 |
45 | ) 46 | }) 47 | } 48 |
49 |
50 | ); 51 | } -------------------------------------------------------------------------------- /src/assets/index.ts: -------------------------------------------------------------------------------- 1 | // LOGOS 2 | export { default as myUsdcLogo } from "./myUsdcLogo.png"; 3 | export { default as myUsdcAltLogo } from "./myUsdcLogo.png"; 4 | export { default as coinbaseLogo } from "./coinbaseLogo.png"; 5 | 6 | export { default as usdc01 } from "./coins/usdc01.png"; 7 | export { default as usdc02 } from "./coins/usdc02.png"; 8 | export { default as usdc03 } from "./coins/usdc03.png"; 9 | export { default as usdc04 } from "./coins/usdc04.png"; 10 | 11 | export { default as historyIcon } from "./historyIcon.svg"; 12 | export { default as historyActiveIcon } from "./historyActiveIcon.svg"; 13 | export { default as walletIcon } from "./walletIcon.svg"; 14 | export { default as walletActiveIcon } from "./walletActiveIcon.svg"; 15 | export { default as profileIcon } from "./profileIcon.svg"; 16 | export { default as profileActiveIcon } from "./profileActiveIcon.svg"; 17 | export { default as profileAltIcon } from "./profileAltIcon.svg"; 18 | export { default as backIcon } from "./backIcon.svg"; 19 | export { default as amexLogo } from "./amexLogo.svg"; 20 | export { default as qrIcon } from "./qrIcon.svg"; 21 | export { default as buyIcon } from "./buyIcon.svg"; 22 | export { default as sendIcon } from "./sendIcon.svg"; 23 | export { default as radioSelectedIcon } from "./radioSelectedIcon.svg"; 24 | export { default as copyIcon } from "./copyIcon.svg"; 25 | export { default as infoIcon } from "./infoIcon.svg"; 26 | export { default as negativeArrowIcon } from "./negativeArrowIcon.svg"; 27 | export { default as positiveArrowIcon } from "./positiveArrowIcon.svg"; 28 | export { default as successCheckIcon } from "./successCheckIcon.svg"; 29 | export { default as nextIcon } from "./nextIcon.svg"; 30 | export { default as warningIcon } from "./warningIcon.svg"; 31 | 32 | // FLAGS 33 | export { default as canadaFlagBg } from "./flags/canadaFlagBg.png"; 34 | export { default as australiaFlagBg } from "./flags/australiaFlagBg.png"; 35 | export { default as britainFlagBg } from "./flags/britainFlagBg.png"; 36 | export { default as canadaFlag } from "./flags/canadaFlag.svg"; 37 | export { default as australiaFlag } from "./flags/australiaFlag.svg"; 38 | export { default as britainFlag } from "./flags/britainFlag.svg"; 39 | 40 | // ONBOARDING 41 | export { default as onboarding01 } from "./onboarding/onboarding01.png"; 42 | export { default as onboarding02 } from "./onboarding/onboarding02.png"; 43 | export { default as onboarding03 } from "./onboarding/onboarding03.png"; 44 | 45 | // AVATARS 46 | export { default as avatar01 } from "./avatars/avatar01.png"; 47 | export { default as avatar02 } from "./avatars/avatar02.png"; 48 | export { default as avatar03 } from "./avatars/avatar03.png"; 49 | export { default as avatar04 } from "./avatars/avatar04.png"; 50 | export { default as avatar05 } from "./avatars/avatar05.png"; 51 | -------------------------------------------------------------------------------- /src/assets/flags/australiaFlag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/Wallet/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | .cardContainer { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-direction: column; 6 | width: 100%; 7 | background-color: #1E1E1E; 8 | border-radius: 20px; 9 | overflow: hidden; 10 | 11 | .card { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: space-between; 18 | width: 100%; 19 | height: 150px; 20 | border-radius: 0px 0px 20px 20px; 21 | background-image: url("../../../assets/cardDesign.png"); 22 | background-size: cover; 23 | padding: 20px; 24 | 25 | .cardRow { 26 | width: 100%; 27 | display: flex; 28 | align-items: start; 29 | justify-content: space-between; 30 | 31 | .cardInfo { 32 | display: flex; 33 | flex-direction: column; 34 | align-items: start; 35 | justify-content: center; 36 | } 37 | 38 | &.topRow { 39 | img { 40 | margin-top: 10px; 41 | } 42 | } 43 | 44 | &.bottomRow { 45 | align-items: end; 46 | 47 | span { 48 | font-size: xx-large; 49 | font-weight: bold; 50 | display: flex; 51 | align-items: center; 52 | justify-content: start; 53 | min-width: 50%; 54 | } 55 | 56 | img { 57 | margin-bottom: 10px; 58 | cursor: pointer; 59 | } 60 | } 61 | } 62 | 63 | .disclaimer { 64 | display: flex; 65 | align-items: center; 66 | justify-content: start; 67 | font-size: x-small; 68 | text-align: left; 69 | width: 100%; 70 | color: white; 71 | gap: 3px; 72 | opacity: 0.5; 73 | 74 | img { 75 | height: 15px; 76 | width: auto; 77 | } 78 | } 79 | } 80 | 81 | .actionContainer { 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-evenly; 85 | width: 100%; 86 | padding: 10px; 87 | 88 | .actionBttn { 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | gap: 5px; 93 | padding: 10px 15px; 94 | border-radius: 10px; 95 | background-color: black; 96 | outline: none; 97 | border: none; 98 | color: white; 99 | cursor: pointer; 100 | 101 | &:hover { 102 | background-color: darken(white, 95%); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/assets/historyIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Wallet/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import { amexLogo, buyIcon, qrIcon, sendIcon, warningIcon } from "../../../assets"; 2 | import { useAppUser } from "../../../contexts/user.context"; 3 | import styles from "./Card.module.scss"; 4 | import BuyModal from "../BuyModal"; 5 | import { useEffect, useRef, useState } from "react"; 6 | import QrModal from "../QrModal"; 7 | import { useNavigate } from "react-router-dom"; 8 | import Skeleton from 'react-loading-skeleton'; 9 | 10 | export default function Card() { 11 | const { user, isUserLoading, setCardBottom } = useAppUser(); 12 | const navigate = useNavigate(); 13 | const cardRef = useRef(null); 14 | 15 | const [isBuyModalOpen, setIsBuyModalOpen] = useState(false); 16 | const [isQrModalOpen, setIsQrModalOpen] = useState(false); 17 | 18 | // Find bottom of card 19 | useEffect(() => { 20 | if (cardRef && cardRef.current && setCardBottom) { 21 | const cardRect = cardRef.current.getBoundingClientRect(); 22 | console.log("cardRect.bottom: ", cardRect.bottom); 23 | setCardBottom(cardRect.bottom); 24 | } 25 | }, [cardRef, setCardBottom]) 26 | 27 | 28 | return ( 29 |
30 | {/* CARD */} 31 |
32 |
33 |
34 | Sample Bank Express Card 35 | 1234 5678 9101 1123 36 |
37 | Some Bank 38 |
39 |
40 | {isUserLoading 41 | ? $ 42 | : $ {user?.wallet.usdBalance.toLocaleString(undefined, { maximumFractionDigits: 3 })}} 43 | setIsQrModalOpen(true)} src={qrIcon} alt="QR" /> 44 |
45 | {/* DISCLAIMER */} 46 |
47 | Warning 48 | Test balance, not real money 49 |
50 |
51 | {/* ACTIONS */} 52 |
53 | 57 | 61 |
62 | 63 | 64 |
65 | ); 66 | } -------------------------------------------------------------------------------- /src/components/Wallet/BuyModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from "react"; 2 | import { amexLogo, radioSelectedIcon } from "../../../assets"; 3 | import styles from "./BuyModal.module.scss"; 4 | import { useFundWallet } from "../../../hooks/useFundWallet"; 5 | import { Coinbase } from "@coinbase/coinbase-sdk"; 6 | import Modal from "../Modal"; 7 | 8 | export default function BuyModal({ isOpen, setOpen }: { isOpen: boolean, setOpen: Dispatch> }) { 9 | const [amount, setAmount] = useState(); 10 | const { fundWallet, isPending, isSuccess } = useFundWallet(Coinbase.assets.Usdc, amount || 0); 11 | 12 | const handleAmountChange = (e: React.ChangeEvent) => { 13 | const _amount = parseFloat(e.target.value); 14 | setAmount(_amount); 15 | } 16 | 17 | // Close Modal on Success 18 | useEffect(() => { 19 | if (isSuccess) 20 | setOpen(false); 21 | }, [isSuccess]) 22 | 23 | return ( 24 | 25 |
26 | {/* TITLE */} 27 | Buy USDC 28 | {/* AMOUNT INPUT */} 29 | 36 |
37 | {/* QUICK ADD */} 38 |
39 |
setAmount(1)} className={styles.quickOption}>$1
40 |
setAmount(5)} className={styles.quickOption}>$5
41 |
setAmount(10)} className={styles.quickOption}>$10
42 |
setAmount(15)} className={styles.quickOption}>$15
43 |
44 |
45 | {/* CARD SELECTION */} 46 |
47 | Choose Account 48 |
49 |
50 | AMEX 51 | Account **** **** **** 1123 52 |
53 | Radio Selected 54 |
55 |
56 |
57 | 63 |
64 | 65 | ); 66 | } -------------------------------------------------------------------------------- /src/pages/Onboarding/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { nextIcon, onboarding01, onboarding02, onboarding03 } from "../../assets"; 3 | import styles from "./Onboarding.module.scss"; 4 | import { useAppUser } from "../../contexts/user.context"; 5 | import { useEffect, useState } from "react"; 6 | 7 | export default function Onboarding() { 8 | const navigate = useNavigate(); 9 | const { clerkUser } = useAppUser(); 10 | const [step, setStep] = useState(0); 11 | 12 | useEffect(() => { 13 | if (clerkUser?.isLoaded) { 14 | if (clerkUser.isSignedIn) { 15 | navigate('/wallet'); 16 | } 17 | } 18 | }, [clerkUser, clerkUser?.isLoaded, navigate]); 19 | 20 | const nextStep = () => step == 2 ? navigate("/login") : setStep(step + 1); 21 | 22 | return ( 23 |
24 | {/* ONBOARDING-1 */} 25 |
26 | Onboarding Image 27 | 28 | Connect your checking 29 | or savings accounts to 30 | view your cash balances. 31 | 32 |
33 | NEXT 34 | Next 35 |
36 |
37 | {/* ONBOARDING-2 */} 38 |
39 | Onboarding Image 40 | 41 | Buy USDC (USD Coin), 42 | a digital version of the US 43 | dollar that’s fully backed 44 | 1-to-1 by real US dollars. 45 | 46 |
47 | NEXT 48 | Next 49 |
50 |
51 | {/* ONBOARDING-3 */} 52 |
53 | Onboarding Image 54 | 55 | Start earning 3% 56 | USDC rewards, or send 57 | USDC to anyone in the 58 | world at zero cost. 59 | 60 |
61 | GET STARTED 62 | Next 63 |
64 |
65 |
66 | ); 67 | } -------------------------------------------------------------------------------- /src/hooks/useGetRecentContacts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query" 2 | import { getRecentContacts } from "../api" 3 | import { useAuth } from "@clerk/clerk-react" 4 | import { AxiosRequestConfig, AxiosResponse } from "axios"; 5 | import { GetRecentContactsResponse, RecentContact } from "../types/api.types"; 6 | import { avatar01, avatar02, avatar03, avatar04, avatar05 } from "../assets"; 7 | 8 | export const useGetRecentContacts = () => { 9 | const { getToken } = useAuth(); 10 | 11 | const placeholderData: RecentContact[] = [ 12 | { 13 | destinationAddress: 'vitalik.eth', 14 | destinationUser: { 15 | email: undefined, 16 | name: 'Vitalik Buterin', 17 | imageUrl: avatar01, 18 | wallet: { 19 | address: 'vitalik.eth' 20 | } 21 | } 22 | }, 23 | { 24 | destinationAddress: 'dan.base.eth', 25 | destinationUser: { 26 | email: undefined, 27 | name: 'Dan Kim', 28 | imageUrl: avatar02, 29 | wallet: { 30 | address: 'dan.base.eth' 31 | } 32 | } 33 | }, 34 | { 35 | destinationAddress: 'jesse.base.eth', 36 | destinationUser: { 37 | email: undefined, 38 | name: 'Jesse Pollak', 39 | imageUrl: avatar03, 40 | wallet: { 41 | address: 'jesse.base.eth' 42 | } 43 | } 44 | }, 45 | { 46 | destinationAddress: 'yuga.eth', 47 | destinationUser: { 48 | email: undefined, 49 | name: 'Yuga Cohler', 50 | imageUrl: avatar04, 51 | wallet: { 52 | address: 'yuga.eth' 53 | } 54 | } 55 | }, 56 | { 57 | destinationAddress: 'jnix.base.eth', 58 | destinationUser: { 59 | email: undefined, 60 | name: 'Josh Nickerson', 61 | imageUrl: avatar05, 62 | wallet: { 63 | address: 'jnix.base.eth' 64 | } 65 | } 66 | }, 67 | ] 68 | 69 | const initialData = { 70 | data: { recentContacts: placeholderData }, 71 | status: 200, 72 | statusText: 'OK', 73 | headers: {}, 74 | config: {} as AxiosRequestConfig, 75 | } as AxiosResponse 76 | 77 | return useQuery({ 78 | queryKey: ["getRecentContacts"], 79 | queryFn: async () => getRecentContacts((await getToken()) as string), 80 | refetchOnWindowFocus: false, 81 | initialData: initialData, 82 | placeholderData: (prevData) => { 83 | if (prevData?.data.recentContacts.length 84 | && prevData?.data.recentContacts.length > 0) return prevData; 85 | else return initialData; 86 | }, 87 | select: (data) => { 88 | if (data?.data.recentContacts.length 89 | && data?.data.recentContacts.length > 0) return data; 90 | else return initialData; 91 | } 92 | }) 93 | } -------------------------------------------------------------------------------- /src/assets/historyActiveIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/Wallet/BuyModal/BuyModal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/animations"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: start; 8 | width: 100%; 9 | 10 | .fadingHr { 11 | width: 95%; 12 | height: 1px; 13 | border: none; 14 | background: linear-gradient(to right, 15 | rgba(255, 255, 255, 0), 16 | rgba(255, 255, 255, 1) 50%, 17 | rgba(255, 255, 255, 0)); 18 | } 19 | 20 | .title { 21 | color: #797979; 22 | font-weight: 600; 23 | font-size: large; 24 | } 25 | 26 | .amountInput { 27 | width: 100%; 28 | padding: 15px 0px; 29 | text-align: center; 30 | border: none; 31 | outline: none; 32 | background-color: black; 33 | color: white; 34 | font-weight: bold; 35 | font-size: xx-large; 36 | 37 | &::placeholder { 38 | color: darken(#797979, 40%); 39 | } 40 | } 41 | 42 | .quickAddContainer { 43 | display: flex; 44 | align-items: center; 45 | justify-content: space-evenly; 46 | width: 100%; 47 | padding: 20px 0px; 48 | 49 | .quickOption { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | background-color: #1E1E1E; 54 | padding: 5px 0px; 55 | border-radius: 10px; 56 | width: 65px; 57 | cursor: pointer; 58 | 59 | &:hover, 60 | &:active { 61 | background-color: lighten(#1E1E1E, 5%); 62 | } 63 | } 64 | } 65 | 66 | .cardSelectionContainer { 67 | padding: 10px 20px; 68 | display: flex; 69 | flex-direction: column; 70 | align-items: start; 71 | justify-content: center; 72 | width: 100%; 73 | 74 | span { 75 | font-size: x-small; 76 | color: #797979; 77 | } 78 | 79 | .cardOption { 80 | display: flex; 81 | align-items: center; 82 | justify-content: space-between; 83 | width: 100%; 84 | padding: 10px 5px; 85 | border-radius: 10px; 86 | cursor: pointer; 87 | 88 | &:hover { 89 | background-color: darken(white, 95%); 90 | } 91 | 92 | .cardDetails { 93 | display: flex; 94 | align-items: center; 95 | justify-content: center; 96 | gap: 5px; 97 | } 98 | } 99 | } 100 | 101 | .loading { 102 | background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.2) 75%); 103 | background-size: 200% 100%; 104 | animation: shimmer 1.5s infinite; 105 | } 106 | 107 | button { 108 | background-color: white; 109 | color: black; 110 | border-radius: 10px; 111 | padding: 5px 10px; 112 | margin: 20px; 113 | outline: none; 114 | border: none; 115 | cursor: pointer; 116 | 117 | &:hover { 118 | background-color: darken(white, 20%); 119 | } 120 | 121 | &:disabled { 122 | background-color: darken(white, 90%); 123 | cursor: default; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/components/Wallet/ExchangeRate/index.tsx: -------------------------------------------------------------------------------- 1 | import Skeleton from "react-loading-skeleton"; 2 | import { 3 | australiaFlag, 4 | australiaFlagBg, 5 | britainFlag, 6 | britainFlagBg, 7 | canadaFlag, 8 | canadaFlagBg, 9 | negativeArrowIcon, 10 | positiveArrowIcon 11 | } from "../../../assets"; 12 | import { useGetUsdRates } from "../../../hooks/useGetUsdRates"; 13 | import styles from "./ExchangeRate.module.scss"; 14 | 15 | export default function ExchangeRate() { 16 | const { data, isFetching } = useGetUsdRates(); 17 | 18 | return ( 19 |
20 | {/* CANADA */} 21 |
22 |
23 | Canada 24 | Canadian Dollar 25 |
26 |
27 | {isFetching 28 | ? 29 | : 30 | {data?.data.usd.cad 31 | .toLocaleString(undefined, { maximumFractionDigits: 4 }) || "NA"} 32 | } 33 |
34 | NA% 35 | Arrow Up 36 |
37 |
38 |
39 |
40 | {/* AUSTRALIA */} 41 |
42 |
43 | Australia 44 | Australian Dollar 45 |
46 |
47 | 48 | {data?.data.usd.aud 49 | .toLocaleString(undefined, { maximumFractionDigits: 4 }) || "NA"} 50 | 51 |
52 | NA% 53 | Arrow Up 54 |
55 |
56 |
57 |
58 | {/* BRITAIN */} 59 |
60 |
61 | Britain 62 | Great British Pound 63 |
64 |
65 | 66 | {data?.data.usd.gbp 67 | .toLocaleString(undefined, { maximumFractionDigits: 4 }) || "NA"} 68 | 69 |
70 | NA% 71 | Arrow Up 72 |
73 |
74 |
75 |
76 | ); 77 | } -------------------------------------------------------------------------------- /src/pages/Profile/Profile.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | 3 | .main { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: start; 8 | margin-top: 20px; 9 | width: 100%; 10 | gap: 20px; 11 | 12 | .userContainer { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | width: 100%; 17 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 18 | padding: 20px 20px; 19 | border-radius: 10px; 20 | 21 | .userDetails { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: start; 25 | justify-content: center; 26 | 27 | .title { 28 | font-size: x-large; 29 | font-weight: bold; 30 | } 31 | 32 | .subtitle { 33 | font-size: small; 34 | font-weight: 100; 35 | } 36 | } 37 | 38 | .pfpContainer { 39 | height: 70px; 40 | width: 70px; 41 | position: relative; 42 | 43 | .pfp { 44 | height: 100%; 45 | border-radius: 100%; 46 | } 47 | 48 | .qr { 49 | position: absolute; 50 | bottom: -5px; 51 | right: 5px; 52 | height: 25px; 53 | width: auto; 54 | cursor: pointer; 55 | } 56 | } 57 | } 58 | 59 | .actionContainer { 60 | display: flex; 61 | align-items: center; 62 | justify-content: space-between; 63 | width: 100%; 64 | background: linear-gradient(180deg, #1E1E1E 0%, #0F0F0F 54.75%, #1E1E1E 100%); 65 | padding: 20px 20px; 66 | border-radius: 10px; 67 | 68 | .actionRow { 69 | display: flex; 70 | align-items: center; 71 | justify-content: space-between; 72 | width: 100%; 73 | 74 | .details { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | gap: 20px; 79 | 80 | img { 81 | height: 30px; 82 | width: auto; 83 | } 84 | 85 | .content { 86 | display: flex; 87 | flex-direction: column; 88 | align-items: start; 89 | justify-content: center; 90 | 91 | .balance { 92 | font-size: medium; 93 | font-weight: bold; 94 | } 95 | 96 | .subtitle { 97 | font-size: small; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | .bttn { 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | gap: 5px; 109 | padding: 10px 15px; 110 | border-radius: 10px; 111 | outline: none; 112 | border: none; 113 | cursor: pointer; 114 | 115 | &.dark { 116 | color: white; 117 | background-color: black; 118 | 119 | &:hover { 120 | background-color: darken(white, 95%); 121 | } 122 | } 123 | 124 | &.light { 125 | color: black; 126 | background-color: white; 127 | 128 | &:hover { 129 | background-color: darken(white, 10%); 130 | } 131 | } 132 | 133 | } 134 | 135 | .appDetails { 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | flex-direction: column; 140 | 141 | .subtitle { 142 | font-size: small; 143 | color: #AAAA; 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/pages/Send/Send.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables"; 2 | @import "../../styles/animations"; 3 | 4 | .main { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | margin-top: 20px; 10 | width: 100%; 11 | gap: 20px; 12 | 13 | .container { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: start; 17 | justify-content: center; 18 | width: 100%; 19 | gap: 10px; 20 | 21 | span { 22 | font-size: small; 23 | margin-left: 15px; 24 | } 25 | 26 | .note { 27 | color: #797979; 28 | } 29 | 30 | input { 31 | width: 100%; 32 | background-color: black; 33 | padding: 10px 20px; 34 | border: 1px solid white; 35 | border-radius: 10px; 36 | color: white; 37 | } 38 | } 39 | 40 | .amountContainer { 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | flex-direction: column; 45 | width: 100%; 46 | margin-top: 50px; 47 | 48 | span { 49 | font-size: small; 50 | } 51 | 52 | .balance { 53 | margin-top: 10px; 54 | color: #797979; 55 | 56 | .value { 57 | color: white; 58 | font-weight: 100; 59 | } 60 | } 61 | 62 | input { 63 | width: 100%; 64 | padding: 15px 0px; 65 | text-align: center; 66 | border: none; 67 | outline: none; 68 | background-color: black; 69 | color: white; 70 | font-weight: bold; 71 | font-size: xx-large; 72 | 73 | &::placeholder { 74 | color: darken(#797979, 40%); 75 | } 76 | } 77 | } 78 | 79 | .fadingHr { 80 | width: 95%; 81 | height: 1px; 82 | border: none; 83 | background: linear-gradient(to right, 84 | rgba(255, 255, 255, 0), 85 | rgba(255, 255, 255, 1) 50%, 86 | rgba(255, 255, 255, 0)); 87 | } 88 | 89 | .loading { 90 | background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.2) 75%); 91 | background-size: 200% 100%; 92 | animation: shimmer 1.5s infinite; 93 | } 94 | } 95 | 96 | .bttn { 97 | display: flex; 98 | align-items: center; 99 | justify-content: center; 100 | gap: 5px; 101 | padding: 5px 25px; 102 | border-radius: 10px; 103 | outline: none; 104 | border: none; 105 | cursor: pointer; 106 | font-size: medium; 107 | 108 | &.dark { 109 | color: white; 110 | background-color: black; 111 | border: 1px solid white; 112 | 113 | &:hover { 114 | background-color: darken(white, 95%); 115 | } 116 | } 117 | 118 | &.light { 119 | color: black; 120 | background-color: white; 121 | 122 | &:hover { 123 | background-color: darken(white, 10%); 124 | } 125 | } 126 | 127 | &:disabled { 128 | opacity: 0.5; 129 | cursor: default; 130 | } 131 | } 132 | 133 | .successMain { 134 | flex-grow: 1; 135 | display: flex; 136 | align-items: center; 137 | justify-content: center; 138 | flex-direction: column; 139 | gap: 15px; 140 | width: 100%; 141 | margin-top: 40px; 142 | 143 | .title { 144 | font-size: x-large; 145 | font-weight: bold; 146 | } 147 | 148 | .subtitle { 149 | font-size: small; 150 | text-align: center; 151 | font-weight: 100; 152 | } 153 | 154 | .actionContainer { 155 | display: flex; 156 | align-items: center; 157 | justify-content: space-evenly; 158 | width: 100%; 159 | 160 | button { 161 | padding: 10px 20px; 162 | font-size: small; 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /src/pages/Send/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import QuickTransfer from "../../components/Wallet/QuickTransfer"; 3 | import styles from "./Send.module.scss"; 4 | import { useTransferAsset } from "../../hooks/useTransferAsset"; 5 | import { Coinbase } from "@coinbase/coinbase-sdk"; 6 | import { successCheckIcon } from "../../assets"; 7 | import { useNavigate, useSearchParams } from "react-router-dom"; 8 | import { useAppUser } from "../../contexts/user.context"; 9 | 10 | export default function Send() { 11 | const navigate = useNavigate(); 12 | const [params] = useSearchParams(); 13 | const { user } = useAppUser(); 14 | 15 | const [destination, setDestination] = useState(params.get("dest") || ""); 16 | const [amount, setAmount] = useState(); 17 | 18 | const { transferAsset, data, isPending, isSuccess, reset } = 19 | useTransferAsset(destination, Coinbase.assets.Usdc, amount || 0); 20 | 21 | const handleDestinationChange = (e: React.ChangeEvent) => { 22 | const _destination = e.target.value; 23 | setDestination(_destination); 24 | } 25 | 26 | const handleAmountChange = (e: React.ChangeEvent) => { 27 | const _amount = parseFloat(e.target.value); 28 | setAmount(_amount); 29 | } 30 | 31 | const handleSumbit = (e: React.FormEvent) => { 32 | e.preventDefault(); 33 | transferAsset(); 34 | } 35 | 36 | const handleBack = () => { 37 | reset(); 38 | navigate("/wallet"); 39 | } 40 | 41 | const handleViewTransfer = () => { 42 | window.open(data?.data.transactionLink, "_blank"); 43 | } 44 | 45 | useEffect(() => { 46 | setDestination(params.get("dest") || "") 47 | }, [params]) 48 | 49 | if (isSuccess) 50 | return ( 51 |
52 | Congratulations! 53 | You’ve just taken part in the future of finance, 54 | powered by CDP SDK. To learn more about USDC 55 | and the technology powering this demo, check 56 | out our docs 57 | or contact us 58 | or join the CDP Discord 59 | 60 | Success Check 61 | Your transacton was completed successfully! 62 |
63 | 64 | 65 |
66 |
67 | ) 68 | 69 | else 70 | return ( 71 |
72 |
73 | Enter recipient Wallet Address or Email Address* 74 | 81 | *Email address only applicable for registered users 82 |
83 |
84 | Recents 85 | 86 |
87 |
88 | Enter Amount 89 | 96 |
97 | 98 | Balance: 99 | {user?.wallet.usdcBalance} USDC 100 | 101 |
102 | 108 | 109 | ); 110 | } -------------------------------------------------------------------------------- /src/assets/qrIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------