();
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 |
33 |
window.open("https://app.deform.cc/form/30138814-ece7-4a5d-bd30-305b4a687a6f", "__blank")}>
34 |
35 |
Build with us
36 |
37 |
38 |
navigate('/wallet/profile')} className={styles.profile} src={profileAltIcon} alt="Profile" />
39 |
40 | :
41 |
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 |
31 |
Transfers
32 |
33 | {/* WALLET */}
34 |
handleTabChange("")} className={`${styles.tab} ${activeTab == "wallet" ? styles.active : ""}`}>
35 |
36 |
Wallet
37 |
38 | {/* Profile */}
39 |
handleTabChange("profile")} className={`${styles.tab} ${activeTab == "profile" ? styles.active : ""}`}>
40 |
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 |
20 |
21 |
22 |
23 | {/* ACTIONS */}
24 |
25 |
26 |
27 |
28 |
29 | ${user?.wallet.usdcBalance}
30 | USDC Balance
31 |
32 |
33 |
34 |
35 | Buy USDC
36 |
37 |
38 |
39 |
40 |
Sign Out
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 |
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 |
48 | Test balance, not real money
49 |
50 |
51 | {/* ACTIONS */}
52 |
53 |
setIsBuyModalOpen(true)} className={styles.actionBttn}>
54 |
55 | Buy USDC
56 |
57 |
navigate('/wallet/send')} className={styles.actionBttn}>
58 |
59 | Send USDC
60 |
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 |
51 |
Account **** **** **** 1123
52 |
53 |
54 |
55 |
56 |
57 |
fundWallet()}
60 | disabled={isPending || !amount}>
61 | Deposit
62 |
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 |
27 |
28 | Connect your checking
29 | or savings accounts to
30 | view your cash balances.
31 |
32 |
33 |
NEXT
34 |
35 |
36 |
37 | {/* ONBOARDING-2 */}
38 |
39 |
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 |
49 |
50 |
51 | {/* ONBOARDING-3 */}
52 |
53 |
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 |
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 |
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 |
36 |
37 |
38 |
39 |
40 | {/* AUSTRALIA */}
41 |
42 |
43 |
44 |
Australian Dollar
45 |
46 |
47 |
48 | {data?.data.usd.aud
49 | .toLocaleString(undefined, { maximumFractionDigits: 4 }) || "NA"}
50 |
51 |
52 |
NA%
53 |
54 |
55 |
56 |
57 |
58 | {/* BRITAIN */}
59 |
60 |
61 |
62 |
Great British Pound
63 |
64 |
65 |
66 | {data?.data.usd.gbp
67 | .toLocaleString(undefined, { maximumFractionDigits: 4 }) || "NA"}
68 |
69 |
70 |
NA%
71 |
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 |
61 |
Your transacton was completed successfully!
62 |
63 | View Transfer
64 | Back to App
65 |
66 |
67 | )
68 |
69 | else
70 | return (
71 |
109 | );
110 | }
--------------------------------------------------------------------------------
/src/assets/qrIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------