Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/app/(mobile-ui)/profile/identity-verification/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import PageContainer from '@/components/0_Bruddle/PageContainer'
import IdentityVerificationView from '@/components/Profile/views/IdentityVerification.view'

export default function IdentityVerificationPage() {
return (
<PageContainer>
<IdentityVerificationView />
</PageContainer>
)
}
3 changes: 3 additions & 0 deletions src/components/Global/Icons/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { ProcessingIcon } from './processing'
import { FailedIcon } from './failed'
import { ChevronDownIcon } from './chevron-down'
import { DoubleCheckIcon } from './double-check'
import { ShieldIcon } from './shield'
import { QuestionMarkIcon } from './question-mark'

// available icon names
Expand Down Expand Up @@ -119,6 +120,7 @@ export type IconName =
| 'processing'
| 'failed'
| 'chevron-down'
| 'shield'
| 'question-mark'

export interface IconProps extends SVGProps<SVGSVGElement> {
Expand Down Expand Up @@ -186,6 +188,7 @@ const iconComponents: Record<IconName, ComponentType<SVGProps<SVGSVGElement>>> =
processing: ProcessingIcon,
failed: FailedIcon,
'chevron-down': ChevronDownIcon,
shield: ShieldIcon,
'question-mark': QuestionMarkIcon,
}

Expand Down
16 changes: 16 additions & 0 deletions src/components/Global/Icons/shield.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FC, SVGProps } from 'react'

export const ShieldIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
return (
<svg width="17" height="21" viewBox="0 0 17 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8.69141 0.5L0.691406 3.5V9.59C0.691406 14.64 4.10141 19.35 8.69141 20.5C13.2814 19.35 16.6914 14.64 16.6914 9.59V3.5L8.69141 0.5ZM14.6914 9.59C14.6914 13.59 12.1414 17.29 8.69141 18.42C5.24141 17.29 2.69141 13.6 2.69141 9.59V4.89L8.69141 2.64L14.6914 4.89V9.59Z"
fill="currentColor"
/>
<path
d="M7.3982 12.0774L5.98444 10.6636C5.8269 10.5061 5.57647 10.5061 5.41893 10.6636C5.2614 10.8212 5.2614 11.0716 5.41893 11.2291L7.11141 12.9216C7.26894 13.0792 7.52342 13.0792 7.68095 12.9216L11.9626 8.64398C12.1202 8.48645 12.1202 8.23601 11.9626 8.07848C11.8051 7.92094 11.5547 7.92094 11.3971 8.07848L7.3982 12.0774Z"
fill="currentColor"
/>
</svg>
)
}
20 changes: 19 additions & 1 deletion src/components/Profile/components/ProfileMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Card, { CardPosition } from '@/components/Global/Card'
import { Icon, IconName } from '@/components/Global/Icons/Icon'
import Link from 'next/link'
import React from 'react'
import { twMerge } from 'tailwind-merge'

interface ProfileMenuItemProps {
icon: IconName
Expand All @@ -12,6 +13,8 @@ interface ProfileMenuItemProps {
position?: CardPosition
comingSoon?: boolean
isExternalLink?: boolean
endIcon?: IconName
endIconClassName?: string
}

const ProfileMenuItem: React.FC<ProfileMenuItemProps> = ({
Expand All @@ -22,6 +25,8 @@ const ProfileMenuItem: React.FC<ProfileMenuItemProps> = ({
position = 'middle',
comingSoon = false,
isExternalLink,
endIcon,
endIconClassName,
}) => {
const content = (
<div className="flex items-center justify-between py-1">
Expand All @@ -34,7 +39,12 @@ const ProfileMenuItem: React.FC<ProfileMenuItemProps> = ({
{comingSoon ? (
<StatusBadge status="soon" size="medium" />
) : (
<Icon name="chevron-up" size={24} fill="black" className="rotate-90" />
<Icon
name={endIcon ?? 'chevron-up'}
size={24}
fill="black"
className={twMerge(endIcon ? endIconClassName : 'rotate-90')}
/>
)}
</div>
</div>
Expand All @@ -48,6 +58,14 @@ const ProfileMenuItem: React.FC<ProfileMenuItemProps> = ({
)
}

if (onClick) {
return (
<Card position={position} onClick={onClick} className="cursor-pointer p-4 active:bg-grey-4">
{content}
</Card>
)
}

return (
<Link
href={href}
Expand Down
42 changes: 37 additions & 5 deletions src/components/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import ProfileHeader from './components/ProfileHeader'
import ProfileMenuItem from './components/ProfileMenuItem'
import { useRouter } from 'next/navigation'
import { checkIfInternalNavigation } from '@/utils'
import ActionModal from '../Global/ActionModal'
import { useState } from 'react'

export const Profile = () => {
const { logoutUser, isLoggingOut, user } = useAuth()
const [isKycApprovedModalOpen, setIsKycApprovedModalOpen] = useState(false)
const router = useRouter()

const logout = async () => {
Expand All @@ -20,6 +23,8 @@ export const Profile = () => {
const fullName = user?.user.fullName || user?.user?.username || 'Anonymous User'
const username = user?.user.username || 'anonymous'

const isKycApproved = user?.user.kycStatus === 'approved'

return (
<div className="h-full w-full bg-background">
<NavHeader
Expand All @@ -36,11 +41,7 @@ export const Profile = () => {
}}
/>
<div className="space-y-8">
<ProfileHeader
name={fullName || username}
username={username}
isVerified={user?.user.kycStatus === 'approved'}
/>
<ProfileHeader name={fullName || username} username={username} isVerified={isKycApproved} />
<div className="space-y-4">
{/* Menu Item - Invite Entry */}
<ProfileMenuItem
Expand All @@ -53,6 +54,21 @@ export const Profile = () => {
{/* Menu Items - First Group */}
<div>
<ProfileMenuItem icon="user" label="Personal details" href="/profile/edit" position="first" />
<ProfileMenuItem
icon="shield"
label="Identity Verification"
href="/profile/identity-verification"
onClick={() => {
if (isKycApproved) {
setIsKycApprovedModalOpen(true)
} else {
router.push('/profile/identity-verification')
}
}}
position="middle"
endIcon={isKycApproved ? 'check' : undefined}
endIconClassName={isKycApproved ? 'text-success-3 size-4' : undefined}
/>
<ProfileMenuItem
icon="bank"
label="Bank accounts"
Expand Down Expand Up @@ -95,6 +111,22 @@ export const Profile = () => {
</div>
</div>
</div>

<ActionModal
visible={isKycApprovedModalOpen}
onClose={() => setIsKycApprovedModalOpen(false)}
title="You’re already verified"
description="Your identity has already been successfully verified. No further action is needed."
icon="shield"
ctas={[
{
text: 'Go back',
shadowSize: '4',
className: 'md:py-2',
onClick: () => setIsKycApprovedModalOpen(false),
},
]}
/>
</div>
)
}
106 changes: 106 additions & 0 deletions src/components/Profile/views/IdentityVerification.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use client'
import { updateUserById } from '@/app/actions/users'
import { Button } from '@/components/0_Bruddle'
import { UserDetailsForm, UserDetailsFormData } from '@/components/AddMoney/UserDetailsForm'
import ErrorAlert from '@/components/Global/ErrorAlert'
import IframeWrapper from '@/components/Global/IframeWrapper'
import NavHeader from '@/components/Global/NavHeader'
import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerificationInProgressModal'
import { useAuth } from '@/context/authContext'
import { useKycFlow } from '@/hooks/useKycFlow'
import { useRouter } from 'next/navigation'
import React, { useMemo, useRef, useState } from 'react'

const IdentityVerificationView = () => {
const { user, fetchUser } = useAuth()
const router = useRouter()
const formRef = useRef<{ handleSubmit: () => void }>(null)
const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false)
const [isUpdatingUser, setIsUpdatingUser] = useState(false)
const [userUpdateError, setUserUpdateError] = useState<string | null>(null)
const {
iframeOptions,
handleInitiateKyc,
handleIframeClose,
isVerificationProgressModalOpen,
closeVerificationProgressModal,
error: kycError,
isLoading: isKycLoading,
} = useKycFlow()

const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
const lastName = lastNameParts.join(' ')

const initialUserDetails: Partial<UserDetailsFormData> = useMemo(
() => ({
firstName: user?.user.fullName ? firstName : '',
lastName: user?.user.fullName ? lastName : '',
email: user?.user.email ?? '',
}),
[user?.user.fullName, user?.user.email, firstName, lastName]
)

const handleUserDetailsSubmit = async (data: UserDetailsFormData) => {
setIsUpdatingUser(true)
setUserUpdateError(null)
try {
if (!user?.user.userId) throw new Error('User not found')
const result = await updateUserById({
userId: user.user.userId,
fullName: `${data.firstName} ${data.lastName}`,
email: data.email,
})
if (result.error) {
throw new Error(result.error)
}
await fetchUser()
// setStep('kyc')
await handleInitiateKyc()
} catch (error: any) {
setUserUpdateError(error.message)
return { error: error.message }
} finally {
setIsUpdatingUser(false)
}
return {}
}

return (
<div className="flex min-h-[inherit] flex-col">
<NavHeader title="Identity Verification" onPrev={() => router.replace('/profile')} />
<div className="my-auto">
<h1 className="mb-3 font-bold">Provide information to begin verification</h1>
<UserDetailsForm
ref={formRef}
onSubmit={handleUserDetailsSubmit}
isSubmitting={isUpdatingUser}
onValidChange={setIsUserDetailsFormValid}
initialData={initialUserDetails}
/>

<Button
onClick={() => formRef.current?.handleSubmit()}
loading={isUpdatingUser || isKycLoading}
variant="purple"
shadowSize="4"
className="mt-3 w-full"
disabled={!isUserDetailsFormValid || isUpdatingUser || isKycLoading}
icon="check-circle"
>
Verify now
</Button>

{(userUpdateError || kycError) && <ErrorAlert description={userUpdateError ?? kycError ?? ''} />}

<IframeWrapper {...iframeOptions} onClose={handleIframeClose} />

<KycVerificationInProgressModal
isOpen={isVerificationProgressModalOpen}
onClose={closeVerificationProgressModal}
/>
</div>
</div>
)
}

export default IdentityVerificationView
Loading