From 38f83f897c431b3e88646a7403ff0953a36d3902 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 8 Aug 2024 09:48:55 +0200 Subject: [PATCH 01/89] feat: cashout page --- src/app/cashout/page.tsx | 12 ++++++++++++ src/components/Cashout/Cashout.consts.ts | 0 src/components/Cashout/Cashout.tsx | 3 +++ src/components/Cashout/index.ts | 1 + src/components/index.ts | 1 + 5 files changed, 17 insertions(+) create mode 100644 src/app/cashout/page.tsx create mode 100644 src/components/Cashout/Cashout.consts.ts create mode 100644 src/components/Cashout/Cashout.tsx create mode 100644 src/components/Cashout/index.ts diff --git a/src/app/cashout/page.tsx b/src/app/cashout/page.tsx new file mode 100644 index 00000000..1847b4b2 --- /dev/null +++ b/src/app/cashout/page.tsx @@ -0,0 +1,12 @@ +import * as components from '@/components' +import Layout from '@/components/Global/Layout' + +export const dynamic = 'force-dynamic' + +export default function ClaimPage() { + return ( + + + + ) +} diff --git a/src/components/Cashout/Cashout.consts.ts b/src/components/Cashout/Cashout.consts.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Cashout/Cashout.tsx b/src/components/Cashout/Cashout.tsx new file mode 100644 index 00000000..7200d385 --- /dev/null +++ b/src/components/Cashout/Cashout.tsx @@ -0,0 +1,3 @@ +export const Cashout = ({}) => { + return
+} diff --git a/src/components/Cashout/index.ts b/src/components/Cashout/index.ts new file mode 100644 index 00000000..f8e78334 --- /dev/null +++ b/src/components/Cashout/index.ts @@ -0,0 +1 @@ +export * from './Cashout' diff --git a/src/components/index.ts b/src/components/index.ts index 79403b94..bdde7107 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,3 +10,4 @@ export * from './Welcome' export * from './Dashboard' export * from './Claim' export * from './Profile' +export * from './Cashout' From cdfcec86211f7631228617defb432c5be8a0bbfa Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 8 Aug 2024 14:56:03 +0200 Subject: [PATCH 02/89] feat: update apiUrl --- .../api/peanut/get-attachment-info/route.ts | 4 +- .../peanut/submit-claim-link/confirm/route.ts | 3 +- .../peanut/submit-claim-link/init/route.ts | 3 +- .../peanut/submit-direct-transfer/route.ts | 3 +- src/app/api/peanut/user/add-account/route.ts | 3 +- src/app/api/peanut/user/create-user/route.ts | 3 +- src/app/api/peanut/user/fetch-user/route.ts | 3 +- .../api/peanut/user/get-jwt-token/route.ts | 4 +- src/app/api/peanut/user/get-user-id/route.ts | 3 +- src/app/api/peanut/user/get-user/route.ts | 3 +- src/app/api/peanut/user/login-user/route.ts | 42 +++++++++++++++ .../api/peanut/user/register-user/route.ts | 51 +++++++++++++++++++ .../peanut/user/submit-profile-photo/route.ts | 3 +- src/app/api/peanut/user/update-user/route.ts | 3 +- src/components/Claim/useClaimLink.tsx | 2 +- src/components/Create/useCreateLink.tsx | 2 +- src/constants/general.consts.ts | 2 +- 17 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 src/app/api/peanut/user/login-user/route.ts create mode 100644 src/app/api/peanut/user/register-user/route.ts diff --git a/src/app/api/peanut/get-attachment-info/route.ts b/src/app/api/peanut/get-attachment-info/route.ts index c83e35a6..b8c4884d 100644 --- a/src/app/api/peanut/get-attachment-info/route.ts +++ b/src/app/api/peanut/get-attachment-info/route.ts @@ -1,14 +1,14 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { getRawParamsFromLink, generateKeysFromString } from '@squirrel-labs/peanut-sdk' // Adjust the import paths according to your project structure - +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { const { link } = await request.json() const params = getRawParamsFromLink(link) const { address: pubKey } = generateKeysFromString(params.password) - const response = await fetch('https://api.peanut.to/get-link-details', { + const response = await fetch(`${consts.PEANUT_API_URL}/get-link-details`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/submit-claim-link/confirm/route.ts b/src/app/api/peanut/submit-claim-link/confirm/route.ts index 33e99afe..90e02236 100644 --- a/src/app/api/peanut/submit-claim-link/confirm/route.ts +++ b/src/app/api/peanut/submit-claim-link/confirm/route.ts @@ -1,13 +1,14 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { generateKeysFromString } from '@squirrel-labs/peanut-sdk' +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { const { link, password, txHash, chainId, senderAddress, amountUsd, transaction } = await request.json() const { address: pubKey } = generateKeysFromString(password) - const response = await fetch('https://api.peanut.to/submit-claim-link/complete', { + const response = await fetch(`${consts.PEANUT_API_URL}/submit-claim-link/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/submit-claim-link/init/route.ts b/src/app/api/peanut/submit-claim-link/init/route.ts index 42f91fc3..54432644 100644 --- a/src/app/api/peanut/submit-claim-link/init/route.ts +++ b/src/app/api/peanut/submit-claim-link/init/route.ts @@ -1,6 +1,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { generateKeysFromString } from '@squirrel-labs/peanut-sdk' // Adjust the import paths according to your project structure +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { @@ -22,7 +23,7 @@ export async function POST(request: NextRequest) { apiFormData.append('file', formData.get('attachmentFile') as File) } - const response = await fetch('https://api.peanut.to/submit-claim-link/init', { + const response = await fetch(`${consts.PEANUT_API_URL}/submit-claim-link/init`, { method: 'POST', body: apiFormData, }) diff --git a/src/app/api/peanut/submit-direct-transfer/route.ts b/src/app/api/peanut/submit-direct-transfer/route.ts index 68f1248f..f6a5b07b 100644 --- a/src/app/api/peanut/submit-direct-transfer/route.ts +++ b/src/app/api/peanut/submit-direct-transfer/route.ts @@ -1,11 +1,12 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { const { txHash, chainId, senderAddress, amountUsd, transaction } = await request.json() - const response = await fetch('https://api.peanut.to/submit-direct-transfer', { + const response = await fetch(`${consts.PEANUT_API_URL}/submit-direct-transfer`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/add-account/route.ts b/src/app/api/peanut/user/add-account/route.ts index 9c309b5e..59d4118f 100644 --- a/src/app/api/peanut/user/add-account/route.ts +++ b/src/app/api/peanut/user/add-account/route.ts @@ -1,5 +1,6 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { @@ -12,7 +13,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: Missing required fields', { status: 400 }) } - const response = await fetch(`https://api.peanut.to/user/create-account`, { + const response = await fetch(`${consts.PEANUT_API_URL}/user/create-account`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/create-user/route.ts b/src/app/api/peanut/user/create-user/route.ts index c6cb2190..09ef520a 100644 --- a/src/app/api/peanut/user/create-user/route.ts +++ b/src/app/api/peanut/user/create-user/route.ts @@ -1,5 +1,6 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { try { @@ -12,7 +13,7 @@ export async function POST(request: NextRequest) { return new NextResponse('Bad Request: Missing required fields', { status: 400 }) } - const response = await fetch(`https://api.peanut.to/user/create`, { + const response = await fetch(`${consts.PEANUT_API_URL}/user/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/fetch-user/route.ts b/src/app/api/peanut/user/fetch-user/route.ts index 4a16fd89..2c687ab6 100644 --- a/src/app/api/peanut/user/fetch-user/route.ts +++ b/src/app/api/peanut/user/fetch-user/route.ts @@ -1,5 +1,6 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import * as consts from '@/constants' export async function GET(request: NextRequest) { try { @@ -13,7 +14,7 @@ export async function GET(request: NextRequest) { const uniqueKey = `${Date.now()}-${accountIdentifier}` const response = await fetch( - `https://api.peanut.to/user/fetch?accountIdentifier=${accountIdentifier}&uniqueKey=${uniqueKey}`, + `${consts.PEANUT_API_URL}/user/fetch?accountIdentifier=${accountIdentifier}&uniqueKey=${uniqueKey}`, { method: 'GET', headers: { diff --git a/src/app/api/peanut/user/get-jwt-token/route.ts b/src/app/api/peanut/user/get-jwt-token/route.ts index a45d086c..840174d1 100644 --- a/src/app/api/peanut/user/get-jwt-token/route.ts +++ b/src/app/api/peanut/user/get-jwt-token/route.ts @@ -1,6 +1,6 @@ import { NextResponse, NextRequest } from 'next/server' import { cookies } from 'next/headers' - +import * as consts from '@/constants' export async function POST(request: NextRequest) { const { signature, message } = await request.json() const apiKey = process.env.PEANUT_API_KEY @@ -10,7 +10,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetch('https://api.peanut.to/get-token', { + const response = await fetch(`${consts.PEANUT_API_URL}/get-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/get-user-id/route.ts b/src/app/api/peanut/user/get-user-id/route.ts index bd65752e..648cc0ec 100644 --- a/src/app/api/peanut/user/get-user-id/route.ts +++ b/src/app/api/peanut/user/get-user-id/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { const { accountIdentifier } = await request.json() @@ -9,7 +10,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetch('https://api.peanut.to/get-user-id', { + const response = await fetch(`${consts.PEANUT_API_URL}/get-user-id`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/peanut/user/get-user/route.ts b/src/app/api/peanut/user/get-user/route.ts index f866ce47..6f233284 100644 --- a/src/app/api/peanut/user/get-user/route.ts +++ b/src/app/api/peanut/user/get-user/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { const { authToken } = await request.json() @@ -9,7 +10,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetch('https://api.peanut.to/get-user', { + const response = await fetch(`${consts.PEANUT_API_URL}/get-user`, { method: 'POST', headers: { Authorization: `Bearer ${authToken}`, diff --git a/src/app/api/peanut/user/login-user/route.ts b/src/app/api/peanut/user/login-user/route.ts new file mode 100644 index 00000000..5b5dec98 --- /dev/null +++ b/src/app/api/peanut/user/login-user/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' + +export async function POST(request: NextRequest) { + const { email, password, userId } = await request.json() + const apiKey = process.env.PEANUT_API_KEY + + if (!email || !password || !userId || !apiKey) { + return new NextResponse('Bad Request: missing required parameters', { status: 400 }) + } + + try { + const response = await fetch(`${consts.PEANUT_API_URL}/login-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': apiKey, + }, + body: JSON.stringify({ email, password, userId }), + }) + + if (response.status !== 200) { + return new NextResponse('Error in login-user', { + status: response.status, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + const data = await response.json() + return new NextResponse(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + console.error('Error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/src/app/api/peanut/user/register-user/route.ts b/src/app/api/peanut/user/register-user/route.ts new file mode 100644 index 00000000..76b558de --- /dev/null +++ b/src/app/api/peanut/user/register-user/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' +import { cookies } from 'next/headers' + +export async function POST(request: NextRequest) { + const { email, password, userId } = await request.json() + const apiKey = process.env.PEANUT_API_KEY + + if (!email || !password || !userId || !apiKey) { + return new NextResponse('Bad Request: missing required parameters', { status: 400 }) + } + + try { + const response = await fetch(`${consts.PEANUT_API_URL}/register-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': apiKey, + }, + body: JSON.stringify({ email, password, userId }), + }) + + if (response.status !== 200) { + return new NextResponse('Error in login-user', { + status: response.status, + headers: { + 'Content-Type': 'application/json', + }, + }) + } + + const data = await response.json() + const token = data.token + + // Set the JWT token in a cookie, nextjs requires to do this serverside + cookies().set('jwt-token', token, { + httpOnly: true, + path: '/', + sameSite: 'strict', + }) + return new NextResponse(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + console.error('Error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/src/app/api/peanut/user/submit-profile-photo/route.ts b/src/app/api/peanut/user/submit-profile-photo/route.ts index b03ccdaf..a440fb9a 100644 --- a/src/app/api/peanut/user/submit-profile-photo/route.ts +++ b/src/app/api/peanut/user/submit-profile-photo/route.ts @@ -1,5 +1,6 @@ import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { const formData = await request.formData() @@ -21,7 +22,7 @@ export async function POST(request: NextRequest) { const apiFormData = new FormData() apiFormData.append('file', file) - const response = await fetch('https://api.peanut.to/submit-profile-photo', { + const response = await fetch(`${consts.PEANUT_API_URL}/submit-profile-photo`, { method: 'POST', headers: { Authorization: `Bearer ${token.value}`, diff --git a/src/app/api/peanut/user/update-user/route.ts b/src/app/api/peanut/user/update-user/route.ts index 73cabc13..6dd2487b 100644 --- a/src/app/api/peanut/user/update-user/route.ts +++ b/src/app/api/peanut/user/update-user/route.ts @@ -1,5 +1,6 @@ import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' +import * as consts from '@/constants' export async function POST(request: NextRequest) { const { userId, username } = await request.json() @@ -12,7 +13,7 @@ export async function POST(request: NextRequest) { } try { - const response = await fetch('https://api.peanut.to/update-user', { + const response = await fetch(`${consts.PEANUT_API_URL}/update-user`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/Claim/useClaimLink.tsx b/src/components/Claim/useClaimLink.tsx index 9d00d39e..7ee0831a 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -114,7 +114,7 @@ export const useClaimLink = () => { amountUSD: number }) => { try { - const response = await fetch('https://api.peanut.to/calculate-pts-for-action', { + const response = await fetch(`${consts.PEANUT_API_URL}/calculate-pts-for-action`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/Create/useCreateLink.tsx b/src/components/Create/useCreateLink.tsx index c15e4f1f..fa6e20c4 100644 --- a/src/components/Create/useCreateLink.tsx +++ b/src/components/Create/useCreateLink.tsx @@ -244,7 +244,7 @@ export const useCreateLink = () => { actionType: 'CREATE' | 'TRANSFER' }) => { try { - const response = await fetch('https://api.peanut.to/calculate-pts-for-action', { + const response = await fetch(`${consts.PEANUT_API_URL}/calculate-pts-for-action`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/constants/general.consts.ts b/src/constants/general.consts.ts index 21e0d8fd..28c9e538 100644 --- a/src/constants/general.consts.ts +++ b/src/constants/general.consts.ts @@ -12,7 +12,7 @@ export const ipfsProviderArray = [ 'https://gw3.io/ipfs/', ] -export const PEANUT_API_URL = process.env.PEANUT_API_URL || 'https://api.peanut.to/' +export const PEANUT_API_URL = process.env.PEANUT_API_URL || 'https://api.peanut.to' export const next_proxy_url = '/api/proxy' export const supportedWalletconnectChains = <{ chainId: string; name: string }[]>[ From 288331a48441b10b9e7d026710f6fbfa43e6ee82 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 8 Aug 2024 14:56:59 +0200 Subject: [PATCH 03/89] feat: create cashout scaffold --- src/components/Cashout/Cashout.consts.ts | 27 +++++++++++++ src/components/Cashout/Cashout.tsx | 39 ++++++++++++++++++- .../Cashout/Components/Confirm.view.tsx | 10 +++++ .../Cashout/Components/Initial.view.tsx | 9 +++++ .../Cashout/Components/Success.view.tsx | 9 +++++ src/components/Cashout/Components/index.ts | 3 ++ 6 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/components/Cashout/Components/Confirm.view.tsx create mode 100644 src/components/Cashout/Components/Initial.view.tsx create mode 100644 src/components/Cashout/Components/Success.view.tsx create mode 100644 src/components/Cashout/Components/index.ts diff --git a/src/components/Cashout/Cashout.consts.ts b/src/components/Cashout/Cashout.consts.ts index e69de29b..29649146 100644 --- a/src/components/Cashout/Cashout.consts.ts +++ b/src/components/Cashout/Cashout.consts.ts @@ -0,0 +1,27 @@ +import * as views from './Components' + +export type CashoutScreens = 'INITIAL' | 'CONFIRM' | 'SUCCESS' + +export interface ICashoutScreenState { + screen: CashoutScreens + idx: number +} + +export const INIT_VIEW_STATE: ICashoutScreenState = { + screen: 'INITIAL', + idx: 0, +} + +export const CASHOUT_SCREEN_FLOW: CashoutScreens[] = ['INITIAL', 'CONFIRM', 'SUCCESS'] + +export const CREATE_SCREEN_MAP: { [key in CashoutScreens]: { comp: React.FC } } = { + INITIAL: { comp: views.InitialCashoutView }, + CONFIRM: { comp: views.ConfirmCashoutView }, + SUCCESS: { comp: views.SuccessCashoutView }, +} + +export interface ICashoutScreenProps { + onPrev: () => void + onNext: () => void + onCustom: (screen: CashoutScreens) => void +} diff --git a/src/components/Cashout/Cashout.tsx b/src/components/Cashout/Cashout.tsx index 7200d385..5ca9b6d6 100644 --- a/src/components/Cashout/Cashout.tsx +++ b/src/components/Cashout/Cashout.tsx @@ -1,3 +1,40 @@ +'use client' +import { createElement, useState } from 'react' +import * as _consts from './Cashout.consts' + export const Cashout = ({}) => { - return
+ const [step, setStep] = useState<_consts.ICashoutScreenState>(_consts.INIT_VIEW_STATE) + + const handleOnNext = () => { + if (step.idx === _consts.CASHOUT_SCREEN_FLOW.length - 1) return + const newIdx = step.idx + 1 + setStep(() => ({ + screen: _consts.CASHOUT_SCREEN_FLOW[newIdx], + idx: newIdx, + })) + } + const handleOnPrev = () => { + if (step.idx === 0) return + const newIdx = step.idx - 1 + setStep(() => ({ + screen: _consts.CASHOUT_SCREEN_FLOW[newIdx], + idx: newIdx, + })) + } + const handleOnCustom = (screen: _consts.CashoutScreens) => { + setStep(() => ({ + screen: screen, + idx: _consts.CASHOUT_SCREEN_FLOW.indexOf(screen), + })) + } + + return ( +
+ {createElement(_consts.CREATE_SCREEN_MAP[step.screen].comp, { + onPrev: handleOnPrev, + onNext: handleOnNext, + onCustom: handleOnCustom, + } as _consts.ICashoutScreenProps)} +
+ ) } diff --git a/src/components/Cashout/Components/Confirm.view.tsx b/src/components/Cashout/Components/Confirm.view.tsx new file mode 100644 index 00000000..2aef0e1c --- /dev/null +++ b/src/components/Cashout/Components/Confirm.view.tsx @@ -0,0 +1,10 @@ +import * as _consts from '../Cashout.consts' + +export const ConfirmCashoutView = ({ onNext, onPrev }: _consts.ICashoutScreenProps) => { + return ( +
+ + +
+ ) +} diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx new file mode 100644 index 00000000..1fa41c2b --- /dev/null +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -0,0 +1,9 @@ +import * as _consts from '../Cashout.consts' + +export const InitialCashoutView = ({ onNext }: _consts.ICashoutScreenProps) => { + return ( +
+ +
+ ) +} diff --git a/src/components/Cashout/Components/Success.view.tsx b/src/components/Cashout/Components/Success.view.tsx new file mode 100644 index 00000000..8f3be234 --- /dev/null +++ b/src/components/Cashout/Components/Success.view.tsx @@ -0,0 +1,9 @@ +import * as _consts from '../Cashout.consts' + +export const SuccessCashoutView = ({ onPrev }: _consts.ICashoutScreenProps) => { + return ( +
+ +
+ ) +} diff --git a/src/components/Cashout/Components/index.ts b/src/components/Cashout/Components/index.ts new file mode 100644 index 00000000..63d8e3f0 --- /dev/null +++ b/src/components/Cashout/Components/index.ts @@ -0,0 +1,3 @@ +export * from './Confirm.view' +export * from './Initial.view' +export * from './Success.view' From 605320bcab7ab1b56c3fc95e02968824ae43967e Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 8 Aug 2024 15:05:06 +0200 Subject: [PATCH 04/89] *WIP* feat: user register and login --- src/components/Claim/Claim.consts.ts | 5 + src/components/Claim/Claim.tsx | 7 + src/components/Claim/Link/Initial.view.tsx | 126 +++++++++++------- .../Claim/Link/Offramp/Confirm.view.tsx | 124 ++++++++++++----- src/components/Profile/index.tsx | 2 +- src/context/authContext.tsx | 1 + src/utils/general.utils.ts | 1 + 7 files changed, 186 insertions(+), 80 deletions(-) diff --git a/src/components/Claim/Claim.consts.ts b/src/components/Claim/Claim.consts.ts index 301cbbb4..8904e34d 100644 --- a/src/components/Claim/Claim.consts.ts +++ b/src/components/Claim/Claim.consts.ts @@ -8,6 +8,7 @@ export type ClaimScreens = 'INITIAL' | 'CONFIRM' | 'SUCCESS' export interface IOfframpForm { name: string email: string + password: string recipient: string } @@ -60,6 +61,10 @@ export interface IClaimScreenProps { setOfframpXchainNeeded: (needed: boolean) => void offrampChainAndToken: { chain: string; token: string } setOfframpChainAndToken: (chainAndToken: { chain: string; token: string }) => void + userType: 'NEW' | 'EXISTING' | undefined + setUserType: (type: 'NEW' | 'EXISTING' | undefined) => void + userId: string | undefined + setUserId: (id: string | undefined) => void } export type claimLinkState = 'LOADING' | 'CLAIM' | 'ALREADY_CLAIMED' | 'NOT_FOUND' | 'CLAIM_SENDER' diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 5bb50e92..f17eb251 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -44,6 +44,7 @@ export const Claim = ({}) => { const [offrampForm, setOfframpForm] = useState<_consts.IOfframpForm>({ name: '', email: '', + password: '', recipient: '', }) const [offrampXchainNeeded, setOfframpXchainNeeded] = useState(false) @@ -57,6 +58,8 @@ export const Claim = ({}) => { const { setSelectedChainID, setSelectedTokenAddress } = useContext(context.tokenSelectorContext) + const [userType, setUserType] = useState<'NEW' | 'EXISTING' | undefined>(undefined) + const [userId, setUserId] = useState(undefined) const { address } = useAccount() const { getAttachmentInfo, estimatePoints } = useClaimLink() @@ -253,6 +256,10 @@ export const Claim = ({}) => { setOfframpXchainNeeded, offrampChainAndToken, setOfframpChainAndToken, + userType, + setUserType, + userId, + setUserId, } as _consts.IClaimScreenProps } /> diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 48c44c2d..81cfdbac 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -19,6 +19,7 @@ import * as _interfaces from '../Claim.interfaces' import * as _utils from '../Claim.utils' import { Popover } from '@headlessui/react' import { PopupButton } from '@typeform/embed-react' +import { useAuth } from '@/context/authContext' export const InitialClaimLinkView = ({ onNext, claimLinkData, @@ -43,7 +44,8 @@ export const InitialClaimLinkView = ({ setLiquidationAddress, setPeanutAccount, setPeanutUser, - + setUserType, + setUserId, setOfframpXchainNeeded, setOfframpChainAndToken, }: _consts.IClaimScreenProps) => { @@ -64,6 +66,7 @@ export const InitialClaimLinkView = ({ const { estimatePoints, claimLink } = useClaimLink() const { open } = useWeb3Modal() const { isConnected, address } = useAccount() + const { user, fetchUser, isFetchingUser, updateUserName, submitProfilePhoto } = useAuth() const handleConnectWallet = async () => { if (isConnected && address) { @@ -183,50 +186,83 @@ export const InitialClaimLinkView = ({ }) setLoadingState('Getting KYC status') - const user = await _utils.fetchUser(recipient.name?.replaceAll(' ', '') ?? '') - setPeanutUser(user) - if (user) { - setOfframpForm({ name: user.full_name, email: user.email, recipient: recipient.name ?? '' }) - - console.log(user) - - const account = user.accounts.find( - (account: any) => - account.account_identifier.toLowerCase() === recipient.name?.replaceAll(' ', '').toLowerCase() - ) - setPeanutAccount(account) - const allLiquidationAddresses = await _utils.getLiquidationAddresses(user.bridge_customer_id) - - console.log('allLiquidationAddresses', allLiquidationAddresses) - - console.log(chainName, tokenName) - - console.log(account.bridge_account_id) - - let liquidationAddressDetails = allLiquidationAddresses.find( - (address) => - address.chain === chainName && - address.currency === tokenName && - address.external_account_id === account.bridge_account_id - ) - - console.log(liquidationAddressDetails) - - if (!liquidationAddressDetails) { - liquidationAddressDetails = await _utils.createLiquidationAddress( - user.bridge_customer_id ?? '', - chainName ?? '', - tokenName ?? '', - account.bridge_account_id, - recipientType === 'iban' ? 'sepa' : 'ach', - recipientType === 'iban' ? 'eur' : 'usd' - ) - } + if (!user) { + console.log('hierzo') - setLiquidationAddress(liquidationAddressDetails) - } else { - setOfframpForm({ ...offrampForm, recipient: recipient.name ?? '' }) + const userIdResponse = await fetch('/api/peanut/user/get-user-id', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + accountIdentifier: recipient.name, + }), + }) + + const response = await userIdResponse.json() + + console.log('response', response) + + setUserId(response.userId) + if (response.isNewUser) { + setUserType('NEW') + } else { + setUserType('EXISTING') + } + setOfframpForm({ + name: '', + email: '', + recipient: recipient.name ?? '', + password: '', + }) } + // if (user?.user.kycStatus === 'verified') { + // setOfframpForm({ + // name: user.full_name, + // email: user.email, + // recipient: recipient.name ?? '', + // password: '', + // }) + + // console.log(user) + + // const account = user.accounts.find( + // (account: any) => + // account.account_identifier.toLowerCase() === recipient.name?.replaceAll(' ', '').toLowerCase() + // ) + // setPeanutAccount(account) + // const allLiquidationAddresses = await _utils.getLiquidationAddresses(user.bridge_customer_id) + + // console.log('allLiquidationAddresses', allLiquidationAddresses) + + // console.log(chainName, tokenName) + + // console.log(account.bridge_account_id) + + // let liquidationAddressDetails = allLiquidationAddresses.find( + // (address) => + // address.chain === chainName && + // address.currency === tokenName && + // address.external_account_id === account.bridge_account_id + // ) + + // console.log(liquidationAddressDetails) + + // if (!liquidationAddressDetails) { + // liquidationAddressDetails = await _utils.createLiquidationAddress( + // user.bridge_customer_id ?? '', + // chainName ?? '', + // tokenName ?? '', + // account.bridge_account_id, + // recipientType === 'iban' ? 'sepa' : 'ach', + // recipientType === 'iban' ? 'eur' : 'usd' + // ) + // } + + // setLiquidationAddress(liquidationAddressDetails) + // } else { + // setOfframpForm({ ...offrampForm, recipient: recipient.name ?? '' }) + // } onNext() } catch (error) { @@ -302,7 +338,7 @@ export const InitialClaimLinkView = ({ ? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C' : recipient.address ? recipient.address - : (address ?? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C'), + : address ?? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C', }) setRoutes([...routes, route]) !toToken && !toChain && setSelectedRoute(route) @@ -448,7 +484,7 @@ export const InitialClaimLinkView = ({ { setRecipient({ name, address }) setInputChanging(false) diff --git a/src/components/Claim/Link/Offramp/Confirm.view.tsx b/src/components/Claim/Link/Offramp/Confirm.view.tsx index a0ec7e45..9ba52044 100644 --- a/src/components/Claim/Link/Offramp/Confirm.view.tsx +++ b/src/components/Claim/Link/Offramp/Confirm.view.tsx @@ -41,6 +41,8 @@ export const ConfirmClaimLinkIbanView = ({ setPeanutAccount, peanutUser, setPeanutUser, + userType, + userId, offrampChainAndToken, offrampXchainNeeded, }: _consts.IClaimScreenProps) => { @@ -89,38 +91,78 @@ export const ConfirmClaimLinkIbanView = ({ const handleEmail = async (inputFormData: _consts.IOfframpForm) => { setOfframpForm(inputFormData) setActiveStep(0) - setInitiatedProcess(true) + // setInitiatedProcess(true) - try { - setLoadingState('Getting KYC status') - - let data = await _utils.getUserLinks(inputFormData) - setCustomerObject(data) + console.log('inputFormData:', inputFormData) - let { tos_status: tosStatus, kyc_status: kycStatus } = data + if (userType === 'NEW') { + try { + const userRegisterResponse = await fetch('/api/peanut/user/register-user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: inputFormData.email, + password: inputFormData.password, + userId: userId, + }), + }) - if (tosStatus !== 'approved') { - goToNext() - return - } + const userRegister = await userRegisterResponse.json() - if (kycStatus !== 'approved') { - setActiveStep(2) - return - } - recipientType === 'us' && setAddressRequired(true) - const peanutUser = await _utils.createUser(data.customer_id, inputFormData.email, inputFormData.name) - setPeanutUser(peanutUser.user) - setActiveStep(3) - } catch (error: any) { - console.error('Error during the submission process:', error) + console.log('userRegister:', userRegister) + } catch (error) {} + } else if (userType === 'EXISTING') { + try { + const userLoginResponse = await fetch('/api/peanut/user/login-user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: inputFormData.email, + password: inputFormData.password, + userId: userId, + }), + }) - setErrorState({ showError: true, errorMessage: 'An error occurred. Please try again later' }) + const userLogin = await userLoginResponse.json() - setLoadingState('Idle') - } finally { - setLoadingState('Idle') + console.log('userLogin:', userLogin) + } catch (error) {} } + + // try { + // setLoadingState('Getting KYC status') + + // let data = await _utils.getUserLinks(inputFormData) + // setCustomerObject(data) + + // let { tos_status: tosStatus, kyc_status: kycStatus } = data + + // if (tosStatus !== 'approved') { + // goToNext() + // return + // } + + // if (kycStatus !== 'approved') { + // setActiveStep(2) + // return + // } + // recipientType === 'us' && setAddressRequired(true) + // const peanutUser = await _utils.createUser(data.customer_id, inputFormData.email, inputFormData.name) + // setPeanutUser(peanutUser.user) + // setActiveStep(3) + // } catch (error: any) { + // console.error('Error during the submission process:', error) + + // setErrorState({ showError: true, errorMessage: 'An error occurred. Please try again later' }) + + // setLoadingState('Idle') + // } finally { + // setLoadingState('Idle') + // } } const handleTOSStatus = async () => { @@ -379,13 +421,19 @@ export const ConfirmClaimLinkIbanView = ({ case 0: return (
- 0} - /> - {errors.name && {errors.name.message}} + {userType === 'NEW' && ( + <> + 0} + /> + {errors.name && ( + {errors.name.message} + )} + + )} 0} + /> + {errors.email && {errors.email.message}} + + 0} onKeyDown={(e) => { if (e.key === 'Enter') { handleEmail(watchOfframp()) } }} /> - {errors.email && {errors.email.message}}
)} From 067dd4c5d944ce57a9325cb986280ff5931201aa Mon Sep 17 00:00:00 2001 From: pawell24 Date: Tue, 13 Aug 2024 17:41:30 +0200 Subject: [PATCH 07/89] Adding cashout flow --- src/components/Cashout/Cashout.consts.ts | 15 +- src/components/Cashout/Cashout.tsx | 63 +++++++- .../Cashout/Components/Confirm.view.tsx | 70 ++++++++- .../Components/ConfirmCashoutDetails.tsx | 20 +++ .../Cashout/Components/Initial.view.tsx | 148 +++++++++++++++++- .../Cashout/Components/Success.view.tsx | 42 ++++- 6 files changed, 342 insertions(+), 16 deletions(-) create mode 100644 src/components/Cashout/Components/ConfirmCashoutDetails.tsx diff --git a/src/components/Cashout/Cashout.consts.ts b/src/components/Cashout/Cashout.consts.ts index 29649146..b4567bcf 100644 --- a/src/components/Cashout/Cashout.consts.ts +++ b/src/components/Cashout/Cashout.consts.ts @@ -1,7 +1,10 @@ +import { CreateScreens } from '../Create/Create.consts' import * as views from './Components' export type CashoutScreens = 'INITIAL' | 'CONFIRM' | 'SUCCESS' +export type CashoutType = 'bank_transfer' | undefined + export interface ICashoutScreenState { screen: CashoutScreens idx: number @@ -14,14 +17,20 @@ export const INIT_VIEW_STATE: ICashoutScreenState = { export const CASHOUT_SCREEN_FLOW: CashoutScreens[] = ['INITIAL', 'CONFIRM', 'SUCCESS'] -export const CREATE_SCREEN_MAP: { [key in CashoutScreens]: { comp: React.FC } } = { +export const CASHOUT_SCREEN_MAP: { [key in CashoutScreens]: { comp: React.FC } } = { INITIAL: { comp: views.InitialCashoutView }, CONFIRM: { comp: views.ConfirmCashoutView }, - SUCCESS: { comp: views.SuccessCashoutView }, + SUCCESS: { comp: views.CashoutSuccessView }, } export interface ICashoutScreenProps { onPrev: () => void onNext: () => void - onCustom: (screen: CashoutScreens) => void + onCustom: (screen: CreateScreens) => void + tokenValue: string | undefined + setTokenValue: (value: string | undefined) => void + recipient: { address: string | undefined; name: string | undefined } + setRecipient: (recipient: { address: string | undefined; name: string | undefined }) => void + usdValue: string | undefined + setUsdValue: (value: string | undefined) => void } diff --git a/src/components/Cashout/Cashout.tsx b/src/components/Cashout/Cashout.tsx index 5ca9b6d6..5505393c 100644 --- a/src/components/Cashout/Cashout.tsx +++ b/src/components/Cashout/Cashout.tsx @@ -1,9 +1,37 @@ 'use client' import { createElement, useState } from 'react' import * as _consts from './Cashout.consts' +import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' export const Cashout = ({}) => { const [step, setStep] = useState<_consts.ICashoutScreenState>(_consts.INIT_VIEW_STATE) + const [tokenValue, setTokenValue] = useState(undefined) + const [usdValue, setUsdValue] = useState(undefined) + + const [linkDetails, setLinkDetails] = useState() + const [password, setPassword] = useState('') + const [transactionType, setTransactionType] = useState<'not-gasless' | 'gasless'>('not-gasless') + + const [gaslessPayload, setGaslessPayload] = useState() + const [gaslessPayloadMessage, setGaslessPayloadMessage] = useState< + peanutInterfaces.IPreparedEIP712Message | undefined + >() + const [preparedDepositTxs, setPreparedDepositTxs] = useState< + peanutInterfaces.IPrepareDepositTxsResponse | undefined + >() + + const [txHash, setTxHash] = useState('') + const [link, setLink] = useState('') + + const [feeOptions, setFeeOptions] = useState(undefined) + const [transactionCostUSD, setTransactionCostUSD] = useState(undefined) + const [estimatedPoints, setEstimatedPoints] = useState(undefined) + + const [createType, setCreateType] = useState<_consts.CashoutType>(undefined) + const [recipient, setRecipient] = useState<{ address: string | undefined; name: string | undefined }>({ + address: undefined, + name: undefined, + }) const handleOnNext = () => { if (step.idx === _consts.CASHOUT_SCREEN_FLOW.length - 1) return @@ -30,11 +58,40 @@ export const Cashout = ({}) => { return (
- {createElement(_consts.CREATE_SCREEN_MAP[step.screen].comp, { + {createElement(_consts.CASHOUT_SCREEN_MAP[step.screen].comp, { onPrev: handleOnPrev, onNext: handleOnNext, - onCustom: handleOnCustom, - } as _consts.ICashoutScreenProps)} + tokenValue: tokenValue, + setTokenValue: setTokenValue, + linkDetails: linkDetails, + setLinkDetails: setLinkDetails, + password: password, + setPassword: setPassword, + transactionType: transactionType, + setTransactionType: setTransactionType, + gaslessPayload: gaslessPayload, + setGaslessPayload: setGaslessPayload, + gaslessPayloadMessage: gaslessPayloadMessage, + setGaslessPayloadMessage: setGaslessPayloadMessage, + preparedDepositTxs: preparedDepositTxs, + setPreparedDepositTxs: setPreparedDepositTxs, + txHash: txHash, + setTxHash: setTxHash, + link: link, + setLink: setLink, + feeOptions, + setFeeOptions, + transactionCostUSD, + setTransactionCostUSD, + estimatedPoints, + setEstimatedPoints, + createType, + setCreateType, + recipient, + setRecipient, + usdValue, + setUsdValue, + } as any)}
) } diff --git a/src/components/Cashout/Components/Confirm.view.tsx b/src/components/Cashout/Components/Confirm.view.tsx index 2aef0e1c..062c1641 100644 --- a/src/components/Cashout/Components/Confirm.view.tsx +++ b/src/components/Cashout/Components/Confirm.view.tsx @@ -1,10 +1,72 @@ +'use client' +import { useContext, useState } from 'react' +import * as context from '@/context' import * as _consts from '../Cashout.consts' +import Loading from '@/components/Global/Loading' +import ConfirmCashoutDetails from './ConfirmCashoutDetails' + +export const ConfirmCashoutView = ({ onNext, onPrev, recipient, usdValue }: _consts.ICashoutScreenProps) => { + const [errorState, setErrorState] = useState<{ + showError: boolean + errorMessage: string + }>({ showError: false, errorMessage: '' }) + const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) + + const handleConfirm = async () => { + setLoadingState('Loading') + setErrorState({ + showError: false, + errorMessage: '', + }) + onNext() + } -export const ConfirmCashoutView = ({ onNext, onPrev }: _consts.ICashoutScreenProps) => { return ( -
- - +
+ + + + +
+ + + + +
+
+ +
+ +
+ +
+ +
+ + + {errorState.showError && ( +
+ +
+ )} +
) } diff --git a/src/components/Cashout/Components/ConfirmCashoutDetails.tsx b/src/components/Cashout/Components/ConfirmCashoutDetails.tsx new file mode 100644 index 00000000..0f788eab --- /dev/null +++ b/src/components/Cashout/Components/ConfirmCashoutDetails.tsx @@ -0,0 +1,20 @@ +import * as utils from '@/utils' +interface IConfirmDetailsProps { + tokenAmount: string + tokenPrice?: number + data?: any +} + +export const ConfirmCashoutDetails = ({ tokenAmount }: IConfirmDetailsProps) => { + return ( +
+
+
+ +
+
+
+ ) +} + +export default ConfirmCashoutDetails diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx index 1fa41c2b..ee89bb33 100644 --- a/src/components/Cashout/Components/Initial.view.tsx +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -1,9 +1,151 @@ +'use client' + +import TokenAmountInput from '@/components/Global/TokenAmountInput' +import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' +import { useAccount } from 'wagmi' +import { useWeb3Modal } from '@web3modal/wagmi/react' +import { useState, useContext, useEffect } from 'react' import * as _consts from '../Cashout.consts' +import * as context from '@/context' +import Loading from '@/components/Global/Loading' +import { useBalance } from '@/hooks/useBalance' + +export const InitialCashoutView = ({ + onNext, + tokenValue, + usdValue, + setUsdValue, + setRecipient, +}: _consts.ICashoutScreenProps) => { + const bankAccounts = [ + { name: 'Bank 1', address: 'PL1298391283912' }, + { name: 'Bank 2', address: 'DE12983912839121332432' }, + { name: 'Bank 3', address: 'US1298392339124234' }, + ] + const { selectedTokenPrice, inputDenomination } = useContext(context.tokenSelectorContext) + const { balances, hasFetchedBalances } = useBalance() + + const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) + const [errorState, setErrorState] = useState<{ + showError: boolean + errorMessage: string + }>({ showError: false, errorMessage: '' }) + const [_tokenValue, _setTokenValue] = useState( + inputDenomination === 'TOKEN' ? tokenValue : usdValue + ) + + const { isConnected } = useAccount() + const { open } = useWeb3Modal() + + const handleConnectWallet = async () => { + open() + } + + const [selectedBankAccount, setSelectedBankAccount] = useState(undefined) + + const handleOnNext = async (_inputValue?: string) => { + setLoadingState('Loading') + try { + if (!selectedBankAccount) { + setErrorState({ showError: true, errorMessage: 'Please select a bank account.' }) + setLoadingState('Idle') + return + } + if (!_tokenValue) return + if (inputDenomination === 'TOKEN') { + if (selectedTokenPrice) { + setUsdValue((parseFloat(_tokenValue) * selectedTokenPrice).toString()) + } + } else if (inputDenomination === 'USD') { + if (selectedTokenPrice) { + setUsdValue(parseFloat(_tokenValue).toString()) + } + } + setRecipient({ name: '', address: selectedBankAccount }) + setLoadingState('Idle') + onNext() + } catch (error) { + setErrorState({ showError: true, errorMessage: 'An error occurred. Please try again.' }) + setLoadingState('Idle') + } + } -export const InitialCashoutView = ({ onNext }: _consts.ICashoutScreenProps) => { return ( -
- +
+ + + + +
+ { + if (!isConnected) handleConnectWallet() + else handleOnNext() + }} + /> + + {hasFetchedBalances && balances.length === 0 && ( +
{ + open() + }} + className="cursor-pointer text-h9 underline" + > + ( Buy Tokens ) +
+ )} +
+
+ {bankAccounts.map((account, index) => ( +
+ setSelectedBankAccount(e.target.value)} + /> + +
+ ))} + +
+ +
+ + {errorState.showError && ( +
+ +
+ )} +
) } diff --git a/src/components/Cashout/Components/Success.view.tsx b/src/components/Cashout/Components/Success.view.tsx index 8f3be234..bc0b7468 100644 --- a/src/components/Cashout/Components/Success.view.tsx +++ b/src/components/Cashout/Components/Success.view.tsx @@ -1,9 +1,45 @@ +'use client' +import Icon from '@/components/Global/Icon' +import { useEffect, useState } from 'react' +import Link from 'next/link' import * as _consts from '../Cashout.consts' +import { useSubscription } from '@web3inbox/react' +import { useAccount } from 'wagmi' +import ConfirmCashoutDetails from './ConfirmCashoutDetails' + +export const CashoutSuccessView = ({ recipient, usdValue }: _consts.ICashoutScreenProps) => { + const { address } = useAccount({}) + const { data: subscription } = useSubscription() + const isSubscribed = Boolean(subscription) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + if (isSubscribed && isLoading) { + setIsLoading(false) + } + }, [isSubscribed, address]) -export const SuccessCashoutView = ({ onPrev }: _consts.ICashoutScreenProps) => { return ( -
- +
+ + + + + + +
+ + + + +
+ + + Go to Dashboard +
) } From 1111df4455fefa746a829b9927d45a4484932f29 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 14 Aug 2024 09:03:27 +0200 Subject: [PATCH 08/89] feat: cashout utils --- src/utils/cashout.utils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/utils/cashout.utils.ts diff --git a/src/utils/cashout.utils.ts b/src/utils/cashout.utils.ts new file mode 100644 index 00000000..18b8e30e --- /dev/null +++ b/src/utils/cashout.utils.ts @@ -0,0 +1,19 @@ +import crypto from 'crypto' + +export const hashPassword = ( + password: string +): { + salt: string + hash: string +} => { + const salt = crypto.randomBytes(16).toString('hex') + + const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') + + return { salt, hash } +} + +export const validatePassword = ({ password, salt, hash }: { password: string; salt: string; hash: string }) => { + const hashVerify = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex') + return hash === hashVerify +} From b9c5f8dc559985967bded761f294fd4f2b4cd270 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 14 Aug 2024 18:31:17 +0200 Subject: [PATCH 09/89] feat: claimflow kyc rework --- .../create-external-account/route.ts | 2 +- .../liquidation-address/create/route.ts | 10 + src/app/api/peanut/user/add-account/route.ts | 13 +- src/app/api/peanut/user/login-user/route.ts | 16 +- .../api/peanut/user/register-user/route.ts | 20 +- src/app/api/peanut/user/update-user/route.ts | 5 +- src/components/Claim/Claim.utils.ts | 1 + src/components/Claim/Link/Initial.view.tsx | 50 +++- .../Claim/Link/Offramp/Confirm.view.tsx | 246 +++++++++++------- src/constants/loadingStates.consts.ts | 1 + src/context/authContext.tsx | 101 ++++++- src/interfaces/interfaces.ts | 1 + 12 files changed, 356 insertions(+), 110 deletions(-) diff --git a/src/app/api/bridge/external-account/create-external-account/route.ts b/src/app/api/bridge/external-account/create-external-account/route.ts index 04de77d2..cd3bd894 100644 --- a/src/app/api/bridge/external-account/create-external-account/route.ts +++ b/src/app/api/bridge/external-account/create-external-account/route.ts @@ -22,7 +22,7 @@ export async function POST(request: NextRequest) { if (accountType === 'iban') { body = { iban: { - account_number: accountDetails.accountNumber.replaceAll(' ', ''), + account_number: accountDetails.accountNumber.replace(/\s+/g, ''), bic: accountDetails.bic, country: accountDetails.country, }, diff --git a/src/app/api/bridge/liquidation-address/create/route.ts b/src/app/api/bridge/liquidation-address/create/route.ts index 8232dad3..ffa7101d 100644 --- a/src/app/api/bridge/liquidation-address/create/route.ts +++ b/src/app/api/bridge/liquidation-address/create/route.ts @@ -14,6 +14,16 @@ export async function POST(request: NextRequest) { const idempotencyKey = uuidv4() + console.log({ + customer_id, + chain, + currency, + external_account_id, + destination_payment_rail, + destination_currency, + idempotencyKey, + }) + let response = await fetch(`https://api.bridge.xyz/v0/customers/${customer_id}/liquidation_addresses`, { method: 'POST', headers: { diff --git a/src/app/api/peanut/user/add-account/route.ts b/src/app/api/peanut/user/add-account/route.ts index 59d4118f..849b99e1 100644 --- a/src/app/api/peanut/user/add-account/route.ts +++ b/src/app/api/peanut/user/add-account/route.ts @@ -1,31 +1,34 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import * as consts from '@/constants' +import { cookies } from 'next/headers' export async function POST(request: NextRequest) { try { const body = await request.json() - const { userId, bridgeCustomerId, bridgeAccountId, accountType, accountIdentifier, accountDetails } = body + const { userId, bridgeAccountId, accountType, accountIdentifier } = body const apiKey = process.env.PEANUT_API_KEY! - if (!apiKey || !bridgeCustomerId || !bridgeAccountId || !accountType || !accountIdentifier || !userId) { + const cookieStore = cookies() + const token = cookieStore.get('jwt-token') + + if (!apiKey || !accountType || !accountIdentifier || !userId || !token) { return new NextResponse('Bad Request: Missing required fields', { status: 400 }) } - const response = await fetch(`${consts.PEANUT_API_URL}/user/create-account`, { + const response = await fetch(`${consts.PEANUT_API_URL}/add-account`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'api-key': apiKey, + Authorization: `Bearer ${token.value}`, }, body: JSON.stringify({ userId, - bridgeCustomerId, bridgeAccountId, accountType, accountIdentifier, - accountDetails, }), }) diff --git a/src/app/api/peanut/user/login-user/route.ts b/src/app/api/peanut/user/login-user/route.ts index 5b5dec98..48986d59 100644 --- a/src/app/api/peanut/user/login-user/route.ts +++ b/src/app/api/peanut/user/login-user/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import * as consts from '@/constants' +import { cookies } from 'next/headers' export async function POST(request: NextRequest) { const { email, password, userId } = await request.json() @@ -19,8 +20,10 @@ export async function POST(request: NextRequest) { body: JSON.stringify({ email, password, userId }), }) + const data = await response.json() + if (response.status !== 200) { - return new NextResponse('Error in login-user', { + return new NextResponse(JSON.stringify(data.error), { status: response.status, headers: { 'Content-Type': 'application/json', @@ -28,7 +31,16 @@ export async function POST(request: NextRequest) { }) } - const data = await response.json() + console.log(data.token) + + const token = data.token + + cookies().set('jwt-token', token, { + httpOnly: true, + path: '/', + sameSite: 'strict', + }) + return new NextResponse(JSON.stringify(data), { status: 200, headers: { diff --git a/src/app/api/peanut/user/register-user/route.ts b/src/app/api/peanut/user/register-user/route.ts index 76b558de..05261dab 100644 --- a/src/app/api/peanut/user/register-user/route.ts +++ b/src/app/api/peanut/user/register-user/route.ts @@ -19,17 +19,23 @@ export async function POST(request: NextRequest) { }, body: JSON.stringify({ email, password, userId }), }) + const data = await response.json() if (response.status !== 200) { - return new NextResponse('Error in login-user', { - status: response.status, - headers: { - 'Content-Type': 'application/json', - }, - }) + return new NextResponse( + JSON.stringify({ + error: data.error, + userId: data.userId, + }), + { + status: response.status, + headers: { + 'Content-Type': 'application/json', + }, + } + ) } - const data = await response.json() const token = data.token // Set the JWT token in a cookie, nextjs requires to do this serverside diff --git a/src/app/api/peanut/user/update-user/route.ts b/src/app/api/peanut/user/update-user/route.ts index 6dd2487b..03dc6a74 100644 --- a/src/app/api/peanut/user/update-user/route.ts +++ b/src/app/api/peanut/user/update-user/route.ts @@ -3,12 +3,12 @@ import { NextRequest, NextResponse } from 'next/server' import * as consts from '@/constants' export async function POST(request: NextRequest) { - const { userId, username } = await request.json() + const { userId, username, bridge_customer_id } = await request.json() const apiKey = process.env.PEANUT_API_KEY const cookieStore = cookies() const token = cookieStore.get('jwt-token') - if (!userId || !username || !apiKey || !token) { + if (!userId || !apiKey || !token) { return new NextResponse('Bad Request: missing required parameters', { status: 400 }) } @@ -23,6 +23,7 @@ export async function POST(request: NextRequest) { body: JSON.stringify({ userId, username, + bridge_customer_id, }), }) diff --git a/src/components/Claim/Claim.utils.ts b/src/components/Claim/Claim.utils.ts index 2b670e7a..ebdc58ab 100644 --- a/src/components/Claim/Claim.utils.ts +++ b/src/components/Claim/Claim.utils.ts @@ -320,6 +320,7 @@ export const createLiquidationAddress = async ( }) if (!response.ok) { + console.log(response) throw new Error('Failed to create liquidation address') } diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index aa71685a..c9766831 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -212,9 +212,53 @@ export const InitialClaimLinkView = ({ setOfframpForm({ name: '', email: '', - recipient: recipient.name ?? '', password: '', + recipient: recipient.name ?? '', }) + } else { + console.log('user', user) + + if (user?.user.kycStatus === 'verified') { + const account = user.accounts.find( + (account: any) => + account.account_identifier.toLowerCase() === + recipient.name?.replaceAll(' ', '').toLowerCase() + ) + if (account) { + console.log('account found') // TODO: set peanut account + + const allLiquidationAddresses = await _utils.getLiquidationAddresses( + user?.user?.bridge_customer_id ?? '' + ) + let liquidationAddressDetails = allLiquidationAddresses.find( + (address) => + address.chain === chainName && + address.currency === tokenName && + address.external_account_id === account.bridge_account_id + ) + + console.log(liquidationAddressDetails) + + if (!liquidationAddressDetails) { + liquidationAddressDetails = await _utils.createLiquidationAddress( + user?.user?.bridge_customer_id ?? '', + chainName ?? '', + tokenName ?? '', + account.bridge_account_id, + recipientType === 'iban' ? 'sepa' : 'ach', + recipientType === 'iban' ? 'eur' : 'usd' + ) + } + + setLiquidationAddress(liquidationAddressDetails) + } else { + console.log('account not found') + } + } else { + console.log('user not verified') + } + return + // TODO: handle user that hasnt set name } // if (user?.user.kycStatus === 'verified') { // setOfframpForm({ @@ -338,7 +382,7 @@ export const InitialClaimLinkView = ({ ? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C' : recipient.address ? recipient.address - : (address ?? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C'), + : address ?? '0x04B5f21facD2ef7c7dbdEe7EbCFBC68616adC45C', }) setRoutes([...routes, route]) !toToken && !toChain && setSelectedRoute(route) @@ -484,7 +528,7 @@ export const InitialClaimLinkView = ({ { setRecipient({ name, address }) setInputChanging(false) diff --git a/src/components/Claim/Link/Offramp/Confirm.view.tsx b/src/components/Claim/Link/Offramp/Confirm.view.tsx index 9ba52044..2ddb46c7 100644 --- a/src/components/Claim/Link/Offramp/Confirm.view.tsx +++ b/src/components/Claim/Link/Offramp/Confirm.view.tsx @@ -16,6 +16,7 @@ import useClaimLink from '../../useClaimLink' import * as utils from '@/utils' import { Step, Steps, useSteps } from 'chakra-ui-steps' import * as consts from '@/constants' +import { useAuth } from '@/context/authContext' const steps = [ { label: 'Step 1: Provide personal details' }, @@ -57,6 +58,7 @@ export const ConfirmClaimLinkIbanView = ({ const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) const [initiatedProcess, setInitiatedProcess] = useState(false) const { claimLink, claimLinkXchain } = useClaimLink() + const { user, fetchUser, isFetchingUser, updateUserName, updateBridgeCustomerId, addAccount } = useAuth() const { register: registerOfframp, @@ -92,11 +94,14 @@ export const ConfirmClaimLinkIbanView = ({ setOfframpForm(inputFormData) setActiveStep(0) // setInitiatedProcess(true) + setLoadingState('Getting profile') - console.log('inputFormData:', inputFormData) + // TODO: add validation - if (userType === 'NEW') { - try { + try { + console.log('inputFormData:', inputFormData) + + if (userType === 'NEW') { const userRegisterResponse = await fetch('/api/peanut/user/register-user', { method: 'POST', headers: { @@ -111,10 +116,46 @@ export const ConfirmClaimLinkIbanView = ({ const userRegister = await userRegisterResponse.json() - console.log('userRegister:', userRegister) - } catch (error) {} - } else if (userType === 'EXISTING') { - try { + // If user already exists, login + // TODO: remove duplicate code + if (userRegisterResponse.status === 409) { + console.log(userRegister.userId) + const userLoginResponse = await fetch('/api/peanut/user/login-user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: inputFormData.email, + password: inputFormData.password, + userId: userRegister.userId, + }), + }) + const userLogin = await userLoginResponse.json() + if (userLoginResponse.status !== 200) { + console.log(userLogin) + if (userLogin === 'Invalid email format') { + errors.email = { + message: 'Invalid email format', + type: 'validate', + } + } + if (userLogin === 'Invalid email, userId') { + errors.email = { + message: 'Incorrect email', + type: 'validate', + } + } else if (userLogin === 'Invalid password') { + errors.password = { + message: 'Invalid password', + type: 'validate', + } + } + + return + } + } + } else if (userType === 'EXISTING') { const userLoginResponse = await fetch('/api/peanut/user/login-user', { method: 'POST', headers: { @@ -126,43 +167,72 @@ export const ConfirmClaimLinkIbanView = ({ userId: userId, }), }) - const userLogin = await userLoginResponse.json() - console.log('userLogin:', userLogin) - } catch (error) {} - } - - // try { - // setLoadingState('Getting KYC status') - - // let data = await _utils.getUserLinks(inputFormData) - // setCustomerObject(data) + if (userLoginResponse.status !== 200) { + if (userLogin === 'Invalid email format') { + errors.email = { + message: 'Invalid email format', + type: 'validate', + } + } + if (userLogin === 'Invalid email, userId') { + errors.email = { + message: 'Incorrect email', + type: 'validate', + } + } else if (userLogin === 'Invalid password') { + errors.password = { + message: 'Invalid password', + type: 'validate', + } + } + + return + } + + setLoadingState('Getting KYC status') + } - // let { tos_status: tosStatus, kyc_status: kycStatus } = data + await fetchUser() + + if (user?.user?.bridge_customer_id) { + if ( + user?.accounts.find( + (account) => + account.account_identifier.toLowerCase().replaceAll(' ', '') === + inputFormData.recipient.toLowerCase().replaceAll(' ', '') + ) + ) { + setActiveStep(4) + } else { + setActiveStep(3) + } + } else { + let data = await _utils.getUserLinks(inputFormData) + setCustomerObject(data) - // if (tosStatus !== 'approved') { - // goToNext() - // return - // } + console.log(data) - // if (kycStatus !== 'approved') { - // setActiveStep(2) - // return - // } - // recipientType === 'us' && setAddressRequired(true) - // const peanutUser = await _utils.createUser(data.customer_id, inputFormData.email, inputFormData.name) - // setPeanutUser(peanutUser.user) - // setActiveStep(3) - // } catch (error: any) { - // console.error('Error during the submission process:', error) + let { tos_status: tosStatus, kyc_status: kycStatus } = data - // setErrorState({ showError: true, errorMessage: 'An error occurred. Please try again later' }) + if (tosStatus !== 'approved') { + goToNext() + return + } - // setLoadingState('Idle') - // } finally { - // setLoadingState('Idle') - // } + if (kycStatus !== 'approved') { + setActiveStep(2) + return + } + recipientType === 'us' && setAddressRequired(true) + } + } catch (error: any) { + console.error('Error during the submission process:', error) + setErrorState({ showError: true, errorMessage: 'An error occurred. Please try again later' }) + } finally { + setLoadingState('Idle') + } } const handleTOSStatus = async () => { @@ -205,41 +275,42 @@ export const ConfirmClaimLinkIbanView = ({ try { if (!customerObject) return const { kyc_status: kycStatus, id, kyc_link } = customerObject - if (kycStatus === 'under_review') { - setErrorState({ - showError: true, - errorMessage: 'KYC under review', - }) - } else if (kycStatus === 'rejected') { - setErrorState({ - showError: true, - errorMessage: 'KYC rejected', - }) - } else if (kycStatus !== 'approved') { - setLoadingState('Awaiting KYC confirmation') - console.log('Awaiting KYC confirmation...') - await _utils.awaitStatusCompletion( - id, - 'kyc', - kycStatus, - kyc_link, - setTosLinkOpened, - setKycLinkOpened, - tosLinkOpened, - kycLinkOpened - ) - } else { - console.log('KYC already approved.') - } + // if (kycStatus === 'under_review') { + // setErrorState({ + // showError: true, + // errorMessage: 'KYC under review', + // }) + // } else if (kycStatus === 'rejected') { + // setErrorState({ + // showError: true, + // errorMessage: 'KYC rejected', + // }) + // } else if (kycStatus !== 'approved') { + // setLoadingState('Awaiting KYC confirmation') + // console.log('Awaiting KYC confirmation...') + // await _utils.awaitStatusCompletion( + // id, + // 'kyc', + // kycStatus, + // kyc_link, + // setTosLinkOpened, + // setKycLinkOpened, + // tosLinkOpened, + // kycLinkOpened + // ) + // } else { + // console.log('KYC already approved.') + // } // Get customer ID - const customer = await _utils.getStatus(customerObject.id, 'customer_id') - setCustomerObject({ ...customerObject, customer_id: customer.customer_id }) + // const customer = await _utils.getStatus(customerObject.id, 'customer_id') + // setCustomerObject({ ...customerObject, customer_id: customer.customer_id }) + + // const { email, name } = watchOfframp() - const { email, name } = watchOfframp() - // Create a user in our DB - const peanutUser = await _utils.createUser(customer.customer_id, email, name) - setPeanutUser(peanutUser.user) + // Update peanut user with bridge customer id + const updatedUser = await updateBridgeCustomerId('test123') + console.log('updatedUser:', updatedUser) recipientType === 'us' && setAddressRequired(true) setLoadingState('Idle') @@ -269,7 +340,7 @@ export const ConfirmClaimLinkIbanView = ({ if (recipientType === 'iban') setLoadingState('Linking IBAN') else if (recipientType === 'us') setLoadingState('Linking account') - const customerId = customerObject?.customer_id + const customerId = customerObject?.customer_id ?? user?.user?.bridge_customer_id const accountType = formData.type const accountDetails = accountType === 'iban' @@ -301,7 +372,7 @@ export const ConfirmClaimLinkIbanView = ({ ) const pAccount = await _utils.createAccount( - peanutUser.user_id, + user?.user?.userId ?? '', customerId, data.id, accountType, @@ -312,10 +383,10 @@ export const ConfirmClaimLinkIbanView = ({ setPeanutAccount(pAccount) const liquidationAddressDetails = await _utils.createLiquidationAddress( - customerObject.customer_id ?? '', + customerId, offrampChainAndToken.chain, offrampChainAndToken.token, - data.id, + '42f8dce2-83b3-454e-b7c9-25384bedcfe8', recipientType === 'iban' ? 'sepa' : 'ach', recipientType === 'iban' ? 'eur' : 'usd' ) @@ -421,19 +492,16 @@ export const ConfirmClaimLinkIbanView = ({ case 0: return (
- {userType === 'NEW' && ( - <> - 0} - /> - {errors.name && ( - {errors.name.message} - )} - - )} + <> + 0} + /> + {errors.name && {errors.name.message}} + + {/* TODO: make this not required if is already defined in user object */} 0} @@ -456,6 +524,10 @@ export const ConfirmClaimLinkIbanView = ({ } }} /> + {errors.password && ( + {errors.password.message} + )} + - {errorState.showError && ( diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx index b1aea5d7..6f6a7f2c 100644 --- a/src/components/Cashout/Components/Initial.view.tsx +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -47,7 +47,7 @@ export const InitialCashoutView = ({ const handleOnNext = async (_inputValue?: string) => { setLoadingState('Loading') try { - if (!selectedBankAccount || !newBankAccount) { + if (!selectedBankAccount && !newBankAccount) { setErrorState({ showError: true, errorMessage: 'Please select a bank account.' }) setLoadingState('Idle') return @@ -81,15 +81,19 @@ export const InitialCashoutView = ({ }, [newBankAccount, selectedBankAccount]) return ( -
+
- - +
+ + +
{ @@ -97,7 +101,7 @@ export const InitialCashoutView = ({ else handleOnNext() }} /> - + {hasFetchedBalances && balances.length === 0 && (
{ @@ -113,7 +117,7 @@ export const InitialCashoutView = ({ {bankAccounts.map((account, index) => (
setSelectedBankAccount(account.address)} > ))} -
+