├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .npmrc ├── README.md ├── apps ├── bank-webhook │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── merchant-app │ ├── .eslintrc.js │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── auth │ │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ ├── lib │ │ └── auth.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── provider.tsx │ ├── public │ │ ├── circles.svg │ │ ├── next.svg │ │ ├── turborepo.svg │ │ └── vercel.svg │ ├── tailwind.config.js │ └── tsconfig.json └── user-app │ ├── .env.example │ ├── .eslintrc.js │ ├── README.md │ ├── app │ ├── (dashboard) │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── p2p │ │ │ └── page.tsx │ │ ├── transactions │ │ │ └── page.tsx │ │ └── transfer │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── user │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── lib │ │ ├── actions │ │ │ ├── createOnRamptxn.ts │ │ │ └── p2pTransfer.ts │ │ └── auth.ts │ ├── page.module.css │ └── page.tsx │ ├── components │ ├── AddMoneyCard.tsx │ ├── AppbarClient.tsx │ ├── BalanceCard.tsx │ ├── OnRampTransactions.tsx │ ├── SendCard.tsx │ └── SidebarItem.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── provider.tsx │ ├── public │ ├── circles.svg │ ├── next.svg │ ├── turborepo.svg │ └── vercel.svg │ ├── tailwind.config.js │ └── tsconfig.json ├── docker └── Dockerfile.user ├── package-lock.json ├── package.json ├── packages ├── db │ ├── .env.example │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20240323121305_init │ │ │ │ └── migration.sql │ │ │ ├── 20240324100733_add_merchant │ │ │ │ └── migration.sql │ │ │ ├── 20240324104524_add_merchant │ │ │ │ └── migration.sql │ │ │ ├── 20240324105137_add_password │ │ │ │ └── migration.sql │ │ │ ├── 20240324145646_added_balances_and_onramp │ │ │ │ └── migration.sql │ │ │ ├── 20240330154923_adds_p2p_transfer │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ ├── schema.prisma │ │ └── seed.ts │ └── tsconfig.json ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── store │ ├── package.json │ ├── src │ │ ├── atoms │ │ │ └── balance.ts │ │ └── hooks │ │ │ └── useBalance.ts │ └── tsconfig.json ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── Appbar.tsx │ ├── Center.tsx │ ├── Select.tsx │ ├── TextInput.tsx │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs ├── tsconfig.json └── turbo.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@repo/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build on PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '20' 17 | 18 | - name: Install Dependencies 19 | run: npm install 20 | 21 | - name: Generate prisma client 22 | run: npm run db:generate 23 | 24 | - name: Run Build 25 | run: npm run build 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check Out Repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@v1 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Build and Push Docker image 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./docker/Dockerfile.user 26 | push: true 27 | tags: 100xdevs/week-18-class:latest # Replace with your Docker Hub username and repository 28 | 29 | - name: Verify Pushed Image 30 | run: docker pull 100xdevs/week-18-class:latest # Replace with your Docker Hub username and repository 31 | 32 | - name: Deploy to EC2 33 | uses: appleboy/ssh-action@master 34 | with: 35 | host: ${{ secrets.SSH_HOST }} 36 | username: ${{ secrets.SSH_USERNAME }} 37 | key: ${{ secrets.SSH_KEY }} 38 | script: | 39 | sudo docker pull 100xdevs/week-18-class:latest 40 | sudo docker stop web-app || true 41 | sudo docker rm web-app || true 42 | sudo docker run -d --name web-app -p 3005:3000 100xdevs/week-18-class:latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/.npmrc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - Clone the repo 2 | 3 | ```jsx 4 | git clone https://github.com/100xdevs-cohort-2/week-17-final-code 5 | ``` 6 | 7 | - npm install 8 | - Run postgres either locally or on the cloud (neon.tech) 9 | 10 | ```jsx 11 | docker run -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres 12 | ``` 13 | 14 | - Copy over all .env.example files to .env 15 | - Update .env files everywhere with the right db url 16 | - Go to `packages/db` 17 | - npx prisma migrate dev 18 | - npx prisma db seed 19 | - Go to `apps/user-app` , run `npm run dev` 20 | - Try logging in using phone - 1111111111 , password - alice (See `seed.ts`) -------------------------------------------------------------------------------- /apps/bank-webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bank-webhook", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npx esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js", 8 | "start": "node dist/index.js", 9 | "dev": "npm run build && npm run start" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@repo/db": "*", 16 | "@types/express": "^4.17.21", 17 | "esbuild": "^0.20.2", 18 | "express": "^4.19.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/bank-webhook/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import db from "@repo/db/client"; 3 | const app = express(); 4 | 5 | app.use(express.json()) 6 | 7 | app.post("/hdfcWebhook", async (req, res) => { 8 | //TODO: Add zod validation here? 9 | //TODO: HDFC bank should ideally send us a secret so we know this is sent by them 10 | const paymentInformation: { 11 | token: string; 12 | userId: string; 13 | amount: string 14 | } = { 15 | token: req.body.token, 16 | userId: req.body.user_identifier, 17 | amount: req.body.amount 18 | }; 19 | 20 | try { 21 | await db.$transaction([ 22 | db.balance.updateMany({ 23 | where: { 24 | userId: Number(paymentInformation.userId) 25 | }, 26 | data: { 27 | amount: { 28 | // You can also get this from your DB 29 | increment: Number(paymentInformation.amount) 30 | } 31 | } 32 | }), 33 | db.onRampTransaction.updateMany({ 34 | where: { 35 | token: paymentInformation.token 36 | }, 37 | data: { 38 | status: "Success", 39 | } 40 | }) 41 | ]); 42 | 43 | res.json({ 44 | message: "Captured" 45 | }) 46 | } catch(e) { 47 | console.error(e); 48 | res.status(411).json({ 49 | message: "Error while processing webhook" 50 | }) 51 | } 52 | 53 | }) 54 | 55 | app.listen(3003); -------------------------------------------------------------------------------- /apps/bank-webhook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/merchant-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/merchant-app/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/merchant-app/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | import { authOptions } from "../../../../lib/auth" 3 | 4 | //@ts-ignore 5 | const handler = NextAuth(authOptions) 6 | 7 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /apps/merchant-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/apps/merchant-app/app/favicon.ico -------------------------------------------------------------------------------- /apps/merchant-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /apps/merchant-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { Providers } from "../provider"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Turborepo", 10 | description: "Generated by create turbo", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }): JSX.Element { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/merchant-app/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .vercelLogo { 11 | filter: invert(1); 12 | } 13 | 14 | .description { 15 | display: inherit; 16 | justify-content: inherit; 17 | align-items: inherit; 18 | font-size: 0.85rem; 19 | max-width: var(--max-width); 20 | width: 100%; 21 | z-index: 2; 22 | font-family: var(--font-mono); 23 | } 24 | 25 | .description a { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | gap: 0.5rem; 30 | } 31 | 32 | .description p { 33 | position: relative; 34 | margin: 0; 35 | padding: 1rem; 36 | background-color: rgba(var(--callout-rgb), 0.5); 37 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 38 | border-radius: var(--border-radius); 39 | } 40 | 41 | .code { 42 | font-weight: 700; 43 | font-family: var(--font-mono); 44 | } 45 | 46 | .hero { 47 | display: flex; 48 | position: relative; 49 | place-items: center; 50 | } 51 | 52 | .heroContent { 53 | display: flex; 54 | position: relative; 55 | z-index: 0; 56 | padding-bottom: 4rem; 57 | flex-direction: column; 58 | gap: 2rem; 59 | justify-content: space-between; 60 | align-items: center; 61 | width: auto; 62 | font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, 63 | "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 64 | "Segoe UI Symbol", "Noto Color Emoji"; 65 | padding-top: 48px; 66 | 67 | @media (min-width: 768px) { 68 | padding-top: 4rem; 69 | padding-bottom: 6rem; 70 | } 71 | @media (min-width: 1024px) { 72 | padding-top: 5rem; 73 | padding-bottom: 8rem; 74 | } 75 | } 76 | 77 | .logos { 78 | display: flex; 79 | z-index: 50; 80 | justify-content: center; 81 | align-items: center; 82 | width: 100%; 83 | } 84 | 85 | .grid { 86 | display: grid; 87 | grid-template-columns: repeat(4, minmax(25%, auto)); 88 | max-width: 100%; 89 | width: var(--max-width); 90 | } 91 | 92 | .card { 93 | padding: 1rem 1.2rem; 94 | border-radius: var(--border-radius); 95 | background: rgba(var(--card-rgb), 0); 96 | border: 1px solid rgba(var(--card-border-rgb), 0); 97 | transition: background 200ms, border 200ms; 98 | } 99 | 100 | .card span { 101 | display: inline-block; 102 | transition: transform 200ms; 103 | } 104 | 105 | .card h2 { 106 | font-weight: 600; 107 | margin-bottom: 0.7rem; 108 | } 109 | 110 | .card p { 111 | margin: 0; 112 | opacity: 0.6; 113 | font-size: 0.9rem; 114 | line-height: 1.5; 115 | max-width: 30ch; 116 | } 117 | 118 | @media (prefers-reduced-motion) { 119 | .card:hover span { 120 | transform: none; 121 | } 122 | } 123 | 124 | /* Mobile */ 125 | @media (max-width: 700px) { 126 | .content { 127 | padding: 4rem; 128 | } 129 | 130 | .grid { 131 | grid-template-columns: 1fr; 132 | margin-bottom: 120px; 133 | max-width: 320px; 134 | text-align: center; 135 | } 136 | 137 | .card { 138 | padding: 1rem 2.5rem; 139 | } 140 | 141 | .card h2 { 142 | margin-bottom: 0.5rem; 143 | } 144 | 145 | .center { 146 | padding: 8rem 0 6rem; 147 | } 148 | 149 | .center::before { 150 | transform: none; 151 | height: 300px; 152 | } 153 | 154 | .description { 155 | font-size: 0.8rem; 156 | } 157 | 158 | .description a { 159 | padding: 1rem; 160 | } 161 | 162 | .description p, 163 | .description div { 164 | display: flex; 165 | justify-content: center; 166 | position: fixed; 167 | width: 100%; 168 | } 169 | 170 | .description p { 171 | align-items: center; 172 | inset: 0 0 auto; 173 | padding: 2rem 1rem 1.4rem; 174 | border-radius: 0; 175 | border: none; 176 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 177 | background: linear-gradient( 178 | to bottom, 179 | rgba(var(--background-start-rgb), 1), 180 | rgba(var(--callout-rgb), 0.5) 181 | ); 182 | background-clip: padding-box; 183 | backdrop-filter: blur(24px); 184 | } 185 | 186 | .description div { 187 | align-items: flex-end; 188 | pointer-events: none; 189 | inset: auto 0 0; 190 | padding: 2rem; 191 | height: 200px; 192 | background: linear-gradient( 193 | to bottom, 194 | transparent 0%, 195 | rgb(var(--background-end-rgb)) 40% 196 | ); 197 | z-index: 1; 198 | } 199 | } 200 | 201 | /* Enable hover only on non-touch devices */ 202 | @media (hover: hover) and (pointer: fine) { 203 | .card:hover { 204 | background: rgba(var(--card-rgb), 0.1); 205 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 206 | } 207 | 208 | .card:hover span { 209 | transform: translateX(4px); 210 | } 211 | } 212 | 213 | .circles { 214 | position: absolute; 215 | min-width: 614px; 216 | min-height: 614px; 217 | pointer-events: none; 218 | } 219 | 220 | .logo { 221 | z-index: 50; 222 | width: 120px; 223 | height: 120px; 224 | } 225 | 226 | .logoGradientContainer { 227 | display: flex; 228 | position: absolute; 229 | z-index: 50; 230 | justify-content: center; 231 | align-items: center; 232 | width: 16rem; 233 | height: 16rem; 234 | } 235 | 236 | .turborepoWordmarkContainer { 237 | display: flex; 238 | z-index: 50; 239 | padding-left: 1.5rem; 240 | padding-right: 1.5rem; 241 | flex-direction: column; 242 | gap: 1.25rem; 243 | justify-content: center; 244 | align-items: center; 245 | text-align: center; 246 | 247 | @media (min-width: 1024px) { 248 | gap: 1.5rem; 249 | } 250 | } 251 | 252 | .turborepoWordmark { 253 | width: 160px; 254 | fill: white; 255 | 256 | @media (min-width: 768px) { 257 | width: 200px; 258 | } 259 | } 260 | 261 | .code { 262 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 263 | monospace; 264 | font-weight: 700; 265 | } 266 | 267 | /* Tablet and Smaller Desktop */ 268 | @media (min-width: 701px) and (max-width: 1120px) { 269 | .grid { 270 | grid-template-columns: repeat(2, 50%); 271 | } 272 | } 273 | 274 | /* Gradients */ 275 | .gradient { 276 | position: absolute; 277 | mix-blend-mode: normal; 278 | will-change: filter; 279 | pointer-events: none; 280 | } 281 | 282 | .gradientSmall { 283 | filter: blur(32px); 284 | } 285 | 286 | .gradientLarge { 287 | filter: blur(75px); 288 | } 289 | 290 | .glowConic { 291 | background-image: var(--glow-conic); 292 | } 293 | 294 | .logoGradient { 295 | opacity: 0.9; 296 | width: 120px; 297 | height: 120px; 298 | } 299 | 300 | .backgroundGradient { 301 | top: -500px; 302 | width: 1000px; 303 | height: 1000px; 304 | opacity: 0.15; 305 | } 306 | 307 | .button { 308 | background-color: #ffffff; 309 | border-radius: 8px; 310 | border-style: none; 311 | box-sizing: border-box; 312 | color: #000000; 313 | cursor: pointer; 314 | display: inline-block; 315 | font-size: 16px; 316 | height: 40px; 317 | line-height: 20px; 318 | list-style: none; 319 | margin: 0; 320 | outline: none; 321 | padding: 10px 16px; 322 | position: relative; 323 | text-align: center; 324 | text-decoration: none; 325 | transition: color 100ms; 326 | vertical-align: baseline; 327 | user-select: none; 328 | -webkit-user-select: none; 329 | touch-action: manipulation; 330 | } 331 | 332 | .button:hover, 333 | .button:focus { 334 | background-color: #e5e4e2; 335 | } 336 | -------------------------------------------------------------------------------- /apps/merchant-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useBalance } from "@repo/store/balance"; 4 | 5 | export default function() { 6 | const balance = useBalance(); 7 | return
8 | hi there {balance} 9 |
10 | } -------------------------------------------------------------------------------- /apps/merchant-app/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import GoogleProvider from "next-auth/providers/google"; 2 | import db from "@repo/db/client"; 3 | 4 | export const authOptions = { 5 | providers: [ 6 | GoogleProvider({ 7 | clientId: process.env.GOOGLE_CLIENT_ID || "", 8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || "" 9 | }) 10 | ], 11 | callbacks: { 12 | async signIn({ user, account }: { 13 | user: { 14 | email: string; 15 | name: string 16 | }, 17 | account: { 18 | provider: "google" | "github" 19 | } 20 | }) { 21 | console.log("hi signin") 22 | if (!user || !user.email) { 23 | return false; 24 | } 25 | 26 | await db.merchant.upsert({ 27 | select: { 28 | id: true 29 | }, 30 | where: { 31 | email: user.email 32 | }, 33 | create: { 34 | email: user.email, 35 | name: user.name, 36 | auth_type: account.provider === "google" ? "Google" : "Github" // Use a prisma type here 37 | }, 38 | update: { 39 | name: user.name, 40 | auth_type: account.provider === "google" ? "Google" : "Github" // Use a prisma type here 41 | } 42 | }); 43 | 44 | return true; 45 | } 46 | }, 47 | secret: process.env.NEXTAUTH_SECRET || "secret" 48 | } -------------------------------------------------------------------------------- /apps/merchant-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/merchant-app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | transpilePackages: ["@repo/ui"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/merchant-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --max-warnings 0" 10 | }, 11 | "dependencies": { 12 | "@repo/db": "*", 13 | "@repo/store": "*", 14 | "@repo/ui": "*", 15 | "next": "^14.1.1", 16 | "next-auth": "^4.24.7", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "recoil": "^0.7.7" 20 | }, 21 | "devDependencies": { 22 | "@next/eslint-plugin-next": "^14.1.1", 23 | "@repo/eslint-config": "*", 24 | "@repo/typescript-config": "*", 25 | "@types/eslint": "^8.56.5", 26 | "@types/node": "^20.11.24", 27 | "@types/react": "^18.2.61", 28 | "@types/react-dom": "^18.2.19", 29 | "eslint": "^8.57.0", 30 | "typescript": "^5.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/merchant-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/merchant-app/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { RecoilRoot } from "recoil"; 3 | import { SessionProvider } from "next-auth/react"; 4 | export const Providers = ({children}: {children: React.ReactNode}) => { 5 | return 6 | 7 | {children} 8 | 9 | 10 | } -------------------------------------------------------------------------------- /apps/merchant-app/public/circles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/merchant-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/merchant-app/public/turborepo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /apps/merchant-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/merchant-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}" 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | } -------------------------------------------------------------------------------- /apps/merchant-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "next-env.d.ts", 12 | "next.config.js", 13 | "**/*.ts", 14 | "**/*.tsx", 15 | ".next/types/**/*.ts" 16 | ], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/user-app/.env.example: -------------------------------------------------------------------------------- 1 | JWT_SECRET=test 2 | NEXTAUTH_URL=http://localhost:3001 3 | -------------------------------------------------------------------------------- /apps/user-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/user-app/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/user-app/app/(dashboard)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function() { 3 | return
4 | Dashboard 5 |
6 | } -------------------------------------------------------------------------------- /apps/user-app/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarItem } from "../../components/SidebarItem"; 2 | 3 | export default function Layout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }): JSX.Element { 8 | return ( 9 |
10 |
11 |
12 | } title="Home" /> 13 | } title="Transfer" /> 14 | } title="Transactions" /> 15 | } title="P2P Transfer" /> 16 |
17 |
18 | {children} 19 |
20 | ); 21 | } 22 | 23 | // Icons Fetched from https://heroicons.com/ 24 | function HomeIcon() { 25 | return 26 | 27 | 28 | } 29 | 30 | function P2PTransferIcon() { 31 | return 32 | 33 | 34 | 35 | } 36 | function TransferIcon() { 37 | return 38 | 39 | 40 | } 41 | 42 | function TransactionsIcon() { 43 | return 44 | 45 | 46 | 47 | } -------------------------------------------------------------------------------- /apps/user-app/app/(dashboard)/p2p/page.tsx: -------------------------------------------------------------------------------- 1 | import { SendCard } from "../../../components/SendCard"; 2 | 3 | export default function() { 4 | return
5 | 6 |
7 | } -------------------------------------------------------------------------------- /apps/user-app/app/(dashboard)/transactions/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | export default function() { 3 | return
4 | Transactions 5 |
6 | } -------------------------------------------------------------------------------- /apps/user-app/app/(dashboard)/transfer/page.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@repo/db/client"; 2 | import { AddMoney } from "../../../components/AddMoneyCard"; 3 | import { BalanceCard } from "../../../components/BalanceCard"; 4 | import { OnRampTransactions } from "../../../components/OnRampTransactions"; 5 | import { getServerSession } from "next-auth"; 6 | import { authOptions } from "../../lib/auth"; 7 | 8 | async function getBalance() { 9 | const session = await getServerSession(authOptions); 10 | const balance = await prisma.balance.findFirst({ 11 | where: { 12 | userId: Number(session?.user?.id) 13 | } 14 | }); 15 | return { 16 | amount: balance?.amount || 0, 17 | locked: balance?.locked || 0 18 | } 19 | } 20 | 21 | async function getOnRampTransactions() { 22 | const session = await getServerSession(authOptions); 23 | const txns = await prisma.onRampTransaction.findMany({ 24 | where: { 25 | userId: Number(session?.user?.id) 26 | } 27 | }); 28 | return txns.map(t => ({ 29 | time: t.startTime, 30 | amount: t.amount, 31 | status: t.status, 32 | provider: t.provider 33 | })) 34 | } 35 | 36 | export default async function() { 37 | const balance = await getBalance(); 38 | const transactions = await getOnRampTransactions(); 39 | 40 | return
41 | hi 42 |
43 | Transfer 44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 |
57 | } -------------------------------------------------------------------------------- /apps/user-app/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | import { authOptions } from "../../../lib/auth" 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /apps/user-app/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth" 2 | import { NextResponse } from "next/server"; 3 | import { authOptions } from "../../lib/auth"; 4 | 5 | export const GET = async () => { 6 | const session = await getServerSession(authOptions); 7 | if (session.user) { 8 | return NextResponse.json({ 9 | user: session.user 10 | }) 11 | } 12 | return NextResponse.json({ 13 | message: "You are not logged in" 14 | }, { 15 | status: 403 16 | }) 17 | } -------------------------------------------------------------------------------- /apps/user-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/100xdevs-cohort-2/week-18-2-ci-cd/65bbb1fa52a6afe0d9916277c7c458ccebcaa05d/apps/user-app/app/favicon.ico -------------------------------------------------------------------------------- /apps/user-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /apps/user-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { Providers } from "../provider"; 5 | import { AppbarClient } from "../components/AppbarClient"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Wallet", 11 | description: "Simple wallet app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }): JSX.Element { 19 | return ( 20 | 21 | 22 | 23 |
24 | 25 | {children} 26 |
27 | 28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/user-app/app/lib/actions/createOnRamptxn.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getServerSession } from "next-auth"; 4 | import { authOptions } from "../auth"; 5 | import prisma from "@repo/db/client"; 6 | 7 | export async function createOnRampTransaction(amount: number, provider: string) { 8 | const session = await getServerSession(authOptions); 9 | const token = Math.random().toString(); 10 | const userId = session.user.id; 11 | if (!userId) { 12 | return { 13 | message: "User not logged in" 14 | } 15 | } 16 | await prisma.onRampTransaction.create({ 17 | data: { 18 | userId: Number(userId), // 1 19 | amount: amount, 20 | status: "Processing", 21 | startTime: new Date(), 22 | provider, 23 | token: token 24 | } 25 | }) 26 | 27 | return { 28 | message: "On ramp transaction added" 29 | } 30 | } -------------------------------------------------------------------------------- /apps/user-app/app/lib/actions/p2pTransfer.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { getServerSession } from "next-auth"; 3 | import { authOptions } from "../auth"; 4 | import prisma from "@repo/db/client"; 5 | 6 | export async function p2pTransfer(to: string, amount: number) { 7 | const session = await getServerSession(authOptions); 8 | const from = session?.user?.id; 9 | if (!from) { 10 | return { 11 | message: "Error while sending" 12 | } 13 | } 14 | const toUser = await prisma.user.findFirst({ 15 | where: { 16 | number: to 17 | } 18 | }); 19 | 20 | if (!toUser) { 21 | return { 22 | message: "User not found" 23 | } 24 | } 25 | await prisma.$transaction(async (tx) => { 26 | await tx.$queryRaw`SELECT * FROM "Balance" WHERE "userId" = ${Number(from)} FOR UPDATE`; 27 | 28 | const fromBalance = await tx.balance.findUnique({ 29 | where: { userId: Number(from) }, 30 | }); 31 | if (!fromBalance || fromBalance.amount < amount) { 32 | throw new Error('Insufficient funds'); 33 | } 34 | 35 | await tx.balance.update({ 36 | where: { userId: Number(from) }, 37 | data: { amount: { decrement: amount } }, 38 | }); 39 | 40 | await tx.balance.update({ 41 | where: { userId: toUser.id }, 42 | data: { amount: { increment: amount } }, 43 | }); 44 | 45 | await tx.p2pTransfer.create({ 46 | data: { 47 | fromUserId: Number(from), 48 | toUserId: toUser.id, 49 | amount, 50 | timestamp: new Date() 51 | } 52 | }) 53 | // locking 54 | }); 55 | } -------------------------------------------------------------------------------- /apps/user-app/app/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import db from "@repo/db/client"; 2 | import CredentialsProvider from "next-auth/providers/credentials" 3 | import bcrypt from "bcrypt"; 4 | 5 | export const authOptions = { 6 | providers: [ 7 | CredentialsProvider({ 8 | name: 'Credentials', 9 | credentials: { 10 | phone: { label: "Phone number", type: "text", placeholder: "1231231231", required: true }, 11 | password: { label: "Password", type: "password", required: true } 12 | }, 13 | // TODO: User credentials type from next-aut 14 | async authorize(credentials: any) { 15 | // Do zod validation, OTP validation here 16 | const hashedPassword = await bcrypt.hash(credentials.password, 10); 17 | const existingUser = await db.user.findFirst({ 18 | where: { 19 | number: credentials.phone 20 | } 21 | }); 22 | 23 | if (existingUser) { 24 | const passwordValidation = await bcrypt.compare(credentials.password, existingUser.password); 25 | if (passwordValidation) { 26 | return { 27 | id: existingUser.id.toString(), 28 | name: existingUser.name, 29 | email: existingUser.number 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | try { 36 | const user = await db.user.create({ 37 | data: { 38 | number: credentials.phone, 39 | password: hashedPassword 40 | } 41 | }); 42 | 43 | return { 44 | id: user.id.toString(), 45 | name: user.name, 46 | email: user.number 47 | } 48 | } catch(e) { 49 | console.error(e); 50 | } 51 | 52 | return null 53 | }, 54 | }) 55 | ], 56 | secret: process.env.JWT_SECRET || "secret", 57 | callbacks: { 58 | // TODO: can u fix the type here? Using any is bad 59 | async session({ token, session }: any) { 60 | session.user.id = token.sub 61 | 62 | return session 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/user-app/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .vercelLogo { 11 | filter: invert(1); 12 | } 13 | 14 | .description { 15 | display: inherit; 16 | justify-content: inherit; 17 | align-items: inherit; 18 | font-size: 0.85rem; 19 | max-width: var(--max-width); 20 | width: 100%; 21 | z-index: 2; 22 | font-family: var(--font-mono); 23 | } 24 | 25 | .description a { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | gap: 0.5rem; 30 | } 31 | 32 | .description p { 33 | position: relative; 34 | margin: 0; 35 | padding: 1rem; 36 | background-color: rgba(var(--callout-rgb), 0.5); 37 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 38 | border-radius: var(--border-radius); 39 | } 40 | 41 | .code { 42 | font-weight: 700; 43 | font-family: var(--font-mono); 44 | } 45 | 46 | .hero { 47 | display: flex; 48 | position: relative; 49 | place-items: center; 50 | } 51 | 52 | .heroContent { 53 | display: flex; 54 | position: relative; 55 | z-index: 0; 56 | padding-bottom: 4rem; 57 | flex-direction: column; 58 | gap: 2rem; 59 | justify-content: space-between; 60 | align-items: center; 61 | width: auto; 62 | font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, 63 | "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 64 | "Segoe UI Symbol", "Noto Color Emoji"; 65 | padding-top: 48px; 66 | 67 | @media (min-width: 768px) { 68 | padding-top: 4rem; 69 | padding-bottom: 6rem; 70 | } 71 | @media (min-width: 1024px) { 72 | padding-top: 5rem; 73 | padding-bottom: 8rem; 74 | } 75 | } 76 | 77 | .logos { 78 | display: flex; 79 | z-index: 50; 80 | justify-content: center; 81 | align-items: center; 82 | width: 100%; 83 | } 84 | 85 | .grid { 86 | display: grid; 87 | grid-template-columns: repeat(4, minmax(25%, auto)); 88 | max-width: 100%; 89 | width: var(--max-width); 90 | } 91 | 92 | .card { 93 | padding: 1rem 1.2rem; 94 | border-radius: var(--border-radius); 95 | background: rgba(var(--card-rgb), 0); 96 | border: 1px solid rgba(var(--card-border-rgb), 0); 97 | transition: background 200ms, border 200ms; 98 | } 99 | 100 | .card span { 101 | display: inline-block; 102 | transition: transform 200ms; 103 | } 104 | 105 | .card h2 { 106 | font-weight: 600; 107 | margin-bottom: 0.7rem; 108 | } 109 | 110 | .card p { 111 | margin: 0; 112 | opacity: 0.6; 113 | font-size: 0.9rem; 114 | line-height: 1.5; 115 | max-width: 30ch; 116 | } 117 | 118 | @media (prefers-reduced-motion) { 119 | .card:hover span { 120 | transform: none; 121 | } 122 | } 123 | 124 | /* Mobile */ 125 | @media (max-width: 700px) { 126 | .content { 127 | padding: 4rem; 128 | } 129 | 130 | .grid { 131 | grid-template-columns: 1fr; 132 | margin-bottom: 120px; 133 | max-width: 320px; 134 | text-align: center; 135 | } 136 | 137 | .card { 138 | padding: 1rem 2.5rem; 139 | } 140 | 141 | .card h2 { 142 | margin-bottom: 0.5rem; 143 | } 144 | 145 | .center { 146 | padding: 8rem 0 6rem; 147 | } 148 | 149 | .center::before { 150 | transform: none; 151 | height: 300px; 152 | } 153 | 154 | .description { 155 | font-size: 0.8rem; 156 | } 157 | 158 | .description a { 159 | padding: 1rem; 160 | } 161 | 162 | .description p, 163 | .description div { 164 | display: flex; 165 | justify-content: center; 166 | position: fixed; 167 | width: 100%; 168 | } 169 | 170 | .description p { 171 | align-items: center; 172 | inset: 0 0 auto; 173 | padding: 2rem 1rem 1.4rem; 174 | border-radius: 0; 175 | border: none; 176 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 177 | background: linear-gradient( 178 | to bottom, 179 | rgba(var(--background-start-rgb), 1), 180 | rgba(var(--callout-rgb), 0.5) 181 | ); 182 | background-clip: padding-box; 183 | backdrop-filter: blur(24px); 184 | } 185 | 186 | .description div { 187 | align-items: flex-end; 188 | pointer-events: none; 189 | inset: auto 0 0; 190 | padding: 2rem; 191 | height: 200px; 192 | background: linear-gradient( 193 | to bottom, 194 | transparent 0%, 195 | rgb(var(--background-end-rgb)) 40% 196 | ); 197 | z-index: 1; 198 | } 199 | } 200 | 201 | /* Enable hover only on non-touch devices */ 202 | @media (hover: hover) and (pointer: fine) { 203 | .card:hover { 204 | background: rgba(var(--card-rgb), 0.1); 205 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 206 | } 207 | 208 | .card:hover span { 209 | transform: translateX(4px); 210 | } 211 | } 212 | 213 | .circles { 214 | position: absolute; 215 | min-width: 614px; 216 | min-height: 614px; 217 | pointer-events: none; 218 | } 219 | 220 | .logo { 221 | z-index: 50; 222 | width: 120px; 223 | height: 120px; 224 | } 225 | 226 | .logoGradientContainer { 227 | display: flex; 228 | position: absolute; 229 | z-index: 50; 230 | justify-content: center; 231 | align-items: center; 232 | width: 16rem; 233 | height: 16rem; 234 | } 235 | 236 | .turborepoWordmarkContainer { 237 | display: flex; 238 | z-index: 50; 239 | padding-left: 1.5rem; 240 | padding-right: 1.5rem; 241 | flex-direction: column; 242 | gap: 1.25rem; 243 | justify-content: center; 244 | align-items: center; 245 | text-align: center; 246 | 247 | @media (min-width: 1024px) { 248 | gap: 1.5rem; 249 | } 250 | } 251 | 252 | .turborepoWordmark { 253 | width: 160px; 254 | fill: white; 255 | 256 | @media (min-width: 768px) { 257 | width: 200px; 258 | } 259 | } 260 | 261 | .code { 262 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 263 | monospace; 264 | font-weight: 700; 265 | } 266 | 267 | /* Tablet and Smaller Desktop */ 268 | @media (min-width: 701px) and (max-width: 1120px) { 269 | .grid { 270 | grid-template-columns: repeat(2, 50%); 271 | } 272 | } 273 | 274 | /* Gradients */ 275 | .gradient { 276 | position: absolute; 277 | mix-blend-mode: normal; 278 | will-change: filter; 279 | pointer-events: none; 280 | } 281 | 282 | .gradientSmall { 283 | filter: blur(32px); 284 | } 285 | 286 | .gradientLarge { 287 | filter: blur(75px); 288 | } 289 | 290 | .glowConic { 291 | background-image: var(--glow-conic); 292 | } 293 | 294 | .logoGradient { 295 | opacity: 0.9; 296 | width: 120px; 297 | height: 120px; 298 | } 299 | 300 | .backgroundGradient { 301 | top: -500px; 302 | width: 1000px; 303 | height: 1000px; 304 | opacity: 0.15; 305 | } 306 | 307 | .button { 308 | background-color: #ffffff; 309 | border-radius: 8px; 310 | border-style: none; 311 | box-sizing: border-box; 312 | color: #000000; 313 | cursor: pointer; 314 | display: inline-block; 315 | font-size: 16px; 316 | height: 40px; 317 | line-height: 20px; 318 | list-style: none; 319 | margin: 0; 320 | outline: none; 321 | padding: 10px 16px; 322 | position: relative; 323 | text-align: center; 324 | text-decoration: none; 325 | transition: color 100ms; 326 | vertical-align: baseline; 327 | user-select: none; 328 | -webkit-user-select: none; 329 | touch-action: manipulation; 330 | } 331 | 332 | .button:hover, 333 | .button:focus { 334 | background-color: #e5e4e2; 335 | } 336 | -------------------------------------------------------------------------------- /apps/user-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth"; 2 | import { redirect } from 'next/navigation' 3 | import { authOptions } from "./lib/auth"; 4 | 5 | export default async function Page() { 6 | const session = await getServerSession(authOptions); 7 | if (session?.user) { 8 | redirect('/dashboard') 9 | } else { 10 | redirect('/api/auth/signin') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/user-app/components/AddMoneyCard.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Button } from "@repo/ui/button"; 3 | import { Card } from "@repo/ui/card"; 4 | import { Center } from "@repo/ui/center"; 5 | import { Select } from "@repo/ui/select"; 6 | import { useState } from "react"; 7 | import { TextInput } from "@repo/ui/textinput"; 8 | import { createOnRampTransaction } from "../app/lib/actions/createOnRamptxn"; 9 | 10 | const SUPPORTED_BANKS = [{ 11 | name: "HDFC Bank", 12 | redirectUrl: "https://netbanking.hdfcbank.com" 13 | }, { 14 | name: "Axis Bank", 15 | redirectUrl: "https://www.axisbank.com/" 16 | }]; 17 | 18 | export const AddMoney = () => { 19 | const [redirectUrl, setRedirectUrl] = useState(SUPPORTED_BANKS[0]?.redirectUrl); 20 | const [amount, setAmount] = useState(0); 21 | const [provider, setProvider] = useState(SUPPORTED_BANKS[0]?.name || ""); 22 | 23 | return 24 |
25 | { 26 | setAmount(Number(value)) 27 | }} /> 28 |
29 | Bank 30 |
31 | { 10 | onSelect(e.target.value) 11 | }} className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> 12 | {options.map(option => )} 13 | 14 | } -------------------------------------------------------------------------------- /packages/ui/src/TextInput.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | export const TextInput = ({ 4 | placeholder, 5 | onChange, 6 | label 7 | }: { 8 | placeholder: string; 9 | onChange: (value: string) => void; 10 | label: string; 11 | }) => { 12 | return
13 | 14 | onChange(e.target.value)} type="text" id="first_name" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder={placeholder} /> 15 |
16 | } -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | interface ButtonProps { 6 | children: ReactNode; 7 | onClick: () => void; 8 | } 9 | 10 | export const Button = ({ onClick, children }: ButtonProps) => { 11 | return ( 12 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ui/src/card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Card({ 4 | title, 5 | children, 6 | }: { 7 | title: string; 8 | children?: React.ReactNode; 9 | }): JSX.Element { 10 | return ( 11 |
14 |

15 | {title} 16 |

17 |

{children}

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/code.tsx: -------------------------------------------------------------------------------- 1 | export function Code({ 2 | children, 3 | className, 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }): JSX.Element { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src", "turbo"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import type { PlopTypes } from "@turbo/gen"; 2 | 3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation 4 | 5 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 6 | // A simple generator to add a new React component to the internal UI library 7 | plop.setGenerator("react-component", { 8 | description: "Adds a new react component", 9 | prompts: [ 10 | { 11 | type: "input", 12 | name: "name", 13 | message: "What is the name of the component?", 14 | }, 15 | ], 16 | actions: [ 17 | { 18 | type: "add", 19 | path: "src/{{kebabCase name}}.tsx", 20 | templateFile: "templates/component.hbs", 21 | }, 22 | { 23 | type: "append", 24 | path: "package.json", 25 | pattern: /"exports": {(?)/g, 26 | template: '"./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",', 27 | }, 28 | ], 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/templates/component.hbs: -------------------------------------------------------------------------------- 1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
4 |

{{ pascalCase name }} Component

5 | {children} 6 |
7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^lint"] 11 | }, 12 | "dev": { 13 | "cache": false, 14 | "persistent": true 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------