Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[READY] 456 Create UI for BRLa offramp KYC #493

Open
wants to merge 53 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4c2e152
implement basic PIXKYCForm
Sharqiewicz Feb 19, 2025
033e649
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
gianfra-t Feb 20, 2025
ce1e17a
Merge branch '471-add-support-for-offramping-brla-digital-token' into…
gianfra-t Feb 20, 2025
dc3f8e5
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
gianfra-t Feb 20, 2025
f3ac457
fixes
gianfra-t Feb 20, 2025
09d28cb
connect BrlaInput and PixKyc components to offramp flow
gianfra-t Feb 21, 2025
3f0dd33
move swap into a separate component
gianfra-t Feb 21, 2025
de6ad1d
add placeholder kyc status spinner
gianfra-t Feb 21, 2025
3f4323f
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
gianfra-t Feb 21, 2025
55b4157
connecting brla offramp with details modal
gianfra-t Feb 21, 2025
e733c4b
refactor PIX form code, add animations
Sharqiewicz Feb 26, 2025
b36feda
animate BRL KYC Form, implement unified Input component
Sharqiewicz Feb 26, 2025
8afc550
change OutputTokenType to enum
Sharqiewicz Feb 27, 2025
23a606b
rename Input to Field as it is related with react-hook-form
Sharqiewicz Feb 27, 2025
9042128
Refactor BrlaSwapFields
Sharqiewicz Feb 27, 2025
fcc22d0
implement BrlaField component
Sharqiewicz Feb 27, 2025
23f8955
implement KYCForm component
Sharqiewicz Feb 27, 2025
8247d16
implement VerificationStatus component (BRLA)
Sharqiewicz Feb 27, 2025
5adf457
implement useBRLAKYCProcess hook
Sharqiewicz Feb 27, 2025
f72318c
add validation for inputs
Sharqiewicz Feb 27, 2025
d2bfa32
refactor BrlaExtendedForm
Sharqiewicz Feb 27, 2025
7d2dcc2
rename comments
Sharqiewicz Feb 27, 2025
f8cc120
fix Field registers react-hook-form
Sharqiewicz Feb 27, 2025
67f7b14
clean useSubmitOfframp
Sharqiewicz Mar 2, 2025
8b48180
add header to KYCForm
Sharqiewicz Mar 2, 2025
824ccb0
add docs for brla.controller getAccount
Sharqiewicz Mar 2, 2025
af4977c
change onSwapConfirm type
Sharqiewicz Mar 3, 2025
e74cbf4
fix TokenDefinition type in swap
Sharqiewicz Mar 3, 2025
39eff84
refactor useBRLAKYCProcess
Sharqiewicz Mar 3, 2025
d81ad38
create brla logic hooks
Sharqiewicz Mar 3, 2025
1e9e15e
fix Spinner styles
Sharqiewicz Mar 3, 2025
8990b25
animate VerificationStatus
Sharqiewicz Mar 3, 2025
c8a618f
show progress kyc after submitting
Sharqiewicz Mar 3, 2025
f1cc7ca
shorten logic brla service
Sharqiewicz Mar 3, 2025
be625d7
remove placeholder birthdate
Sharqiewicz Mar 3, 2025
052d5e4
add basic yup validation
Sharqiewicz Mar 3, 2025
53e34b7
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
Sharqiewicz Mar 4, 2025
d870e64
fix brla form types
Sharqiewicz Mar 4, 2025
caaa916
remove unnecessary comment
Sharqiewicz Mar 4, 2025
3895349
remove unnecessary signer-service hooks.ts brla
Sharqiewicz Mar 4, 2025
4dd5fc5
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
Sharqiewicz Mar 4, 2025
0efce5d
Move brla fields further down in swap form
ebma Mar 4, 2025
929826f
Add placeholder to text fieelds and remove heading
ebma Mar 4, 2025
48ddfdb
Check existence of brla username and password in constructor
ebma Mar 4, 2025
c9702d6
Merge branch 'staging' into 456-create-a-ui-for-brl-offramp-kyc
gianfra-t Mar 4, 2025
5c8db69
add address fields, modify kyc states
gianfra-t Mar 4, 2025
f62ba6b
Merge branch '456-create-a-ui-for-brl-offramp-kyc' of github.com:pend…
gianfra-t Mar 4, 2025
f7d933b
modify store validator, ui states
gianfra-t Mar 5, 2025
362eb86
modifications to kyc status logic from backend
gianfra-t Mar 5, 2025
168258e
add initialize failed message state and action to offramp store, fail…
gianfra-t Mar 5, 2025
cea3a2b
use dialog from ref.current, replace dialog visible setter for global…
gianfra-t Mar 5, 2025
9785e21
fix build, remove comments
gianfra-t Mar 5, 2025
4cbb93f
prevent early return issues
gianfra-t Mar 5, 2025
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"motion": "^12.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.51.5",
"react-hook-form": "^7.54.2",
"react-toastify": "^10.0.6",
"stellar-base": "^11.0.1",
"stellar-sdk": "^13.1.0",
Expand Down
16 changes: 14 additions & 2 deletions signer-service/src/api/controllers/brla.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@ import { Request, Response } from 'express';
import { BrlaApiService } from '../services/brla/brlaApiService';
import { RegisterSubaccountPayload, TriggerOfframpRequest } from '../services/brla/types';
import { eventPoller } from '../..';

/**
* Retrieves a BRLA user's information based on Tax ID
*
* This endpoint fetches a user's subaccount information from the BRLA API service.
* It validates that the user exists and has completed KYC level 1 verification.
* If successful, it returns the user's EVM wallet address which is needed for offramp operations.
*
* @returns void - Sends JSON response with evmAddress on success, or appropriate error status
*
* @throws 400 - If taxId or pixId are missing or if KYC level is invalid
* @throws 404 - If the subaccount cannot be found
* @throws 500 - For any server-side errors during processing
*/
export const getBrlaUser = async (req: Request<{}, {}, {}, { taxId: string }>, res: Response): Promise<void> => {
try {
const { taxId } = req.query;
Expand All @@ -12,6 +24,7 @@ export const getBrlaUser = async (req: Request<{}, {}, {}, { taxId: string }>, r
return;
}

// TODO how to check that pixId is valid, as a later offramp will get stuck if it's not valid..
const brlaApiService = BrlaApiService.getInstance();
const subaccount = await brlaApiService.getSubaccount(taxId);
if (!subaccount) {
Expand Down Expand Up @@ -146,7 +159,6 @@ export const fetchSubaccountKycStatus = async (
res.status(404).json({ error: `No status events found for ${taxId}` });
return;
}

res.status(200).json({ type: lastEventCached.subscription, status: lastEventCached.data.status });
} catch (error) {
console.error('Error while requesting KYC status: ', error);
Expand Down
5 changes: 1 addition & 4 deletions signer-service/src/api/services/brla/brlaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,9 @@ export class BrlaApiService {

return response.json();
}

public async getSubaccount(taxId: string): Promise<SubaccountData | undefined> {
const endpoint = `/subaccounts`;
const query = `taxId=${encodeURIComponent(taxId)}`;
const response = await this.sendRequest(endpoint, 'GET', query);
const response = await this.sendRequest('/subaccounts', 'GET', query);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no right or wrong here but looking at the other functions we should make sure they are also using the same format. The other functions define the endpoint as an extra const so either we do it on all of them, or remove the extra const endpoint on all of them.

return response.subaccounts[0];
}

Expand All @@ -154,7 +152,6 @@ export class BrlaApiService {

public async acknowledgeEvents(ids: string[]): Promise<void> {
const endpoint = `/webhooks/events`;
console.log('Calling acknowledgeEvents with ids: ', ids);
return await this.sendRequest(endpoint, 'PATCH', undefined, { ids });
}
}
85 changes: 85 additions & 0 deletions src/components/BrlaComponents/BrlaExtendedForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { RefObject } from 'react';

import { useKYCProcess } from '../../hooks/brla/useBRLAKYCProcess';
import { useKYCForm } from '../../hooks/brla/useKYCForm';

import { FeeComparisonRef } from '../FeeComparison';
import { VerificationStatus } from './VerificationStatus';
import { BrlaFieldProps, ExtendedBrlaFieldOptions } from './BrlaField';
import { KYCForm } from './KYCForm';

interface PIXKYCFormProps {
feeComparisonRef: RefObject<FeeComparisonRef | null>;
setIsOfframpSummaryDialogVisible: (isVisible: boolean) => void;
onSwapConfirm: () => void;
}

const PIXKYCFORM_FIELDS: BrlaFieldProps[] = [
{
id: ExtendedBrlaFieldOptions.PHONE,
label: 'Phone Number',
type: 'text',
placeholder: 'Phone Number',
required: true,
index: 0,
},
{
id: ExtendedBrlaFieldOptions.ADDRESS,
label: 'Address',
type: 'text',
placeholder: 'Address',
required: true,
index: 1,
},
{
id: ExtendedBrlaFieldOptions.FULL_NAME,
label: 'Full Name',
type: 'text',
placeholder: 'Full Name',
required: true,
index: 2,
},
{
id: ExtendedBrlaFieldOptions.CPF,
label: 'CPF',
type: 'text',
placeholder: 'CPF',
validationPattern: {
value: /^\d{11}$/,
message: 'CPF must be 11 digits',
},
required: true,
index: 3,
},
{
id: ExtendedBrlaFieldOptions.BIRTHDATE,
label: 'Birthdate',
type: 'date',
required: true,
index: 4,
},
];

export const PIXKYCForm = ({ feeComparisonRef, setIsOfframpSummaryDialogVisible }: PIXKYCFormProps) => {
const { verificationStatus, statusMessage, handleFormSubmit, handleBackClick, isSubmitted } = useKYCProcess(
setIsOfframpSummaryDialogVisible,
);

const { kycForm } = useKYCForm();

return (
<div className="relative">
{!isSubmitted ? (
<KYCForm
fields={PIXKYCFORM_FIELDS}
form={kycForm}
onSubmit={handleFormSubmit}
onBackClick={handleBackClick}
feeComparisonRef={feeComparisonRef}
/>
) : (
<VerificationStatus status={verificationStatus} message={statusMessage} />
)}
</div>
);
};
65 changes: 65 additions & 0 deletions src/components/BrlaComponents/BrlaField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FC } from 'react';
import { useFormContext, useFormState } from 'react-hook-form';
import { motion } from 'motion/react';

import { Field, FieldProps } from '../../Field';

export enum StandardBrlaFieldOptions {
TAX_ID = 'taxId',
PIX_ID = 'pixId',
}

export enum ExtendedBrlaFieldOptions {
PHONE = 'phone',
ADDRESS = 'address',
FULL_NAME = 'fullName',
CPF = 'cpf',
BIRTHDATE = 'birthdate',
EMAIL = 'email',
}

export type BrlaFieldOptions = StandardBrlaFieldOptions | ExtendedBrlaFieldOptions;

export interface BrlaFieldProps extends FieldProps {
id: BrlaFieldOptions;
label: string;
index: number;
validationPattern?: {
value: RegExp;
message: string;
};
}

export const BrlaField: FC<BrlaFieldProps> = ({ id, label, index, validationPattern, ...rest }) => {
// It required to be inside a FormProvider (react-hook-form)
const { register } = useFormContext();
const { errors } = useFormState();
const errorMessage = errors[id]?.message as string;

return (
<motion.div
className="mb-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.4,
delay: index * 0.15,
type: 'spring',
stiffness: 300,
damping: 15,
}}
>
<label htmlFor={id} className="block mb-1">
{label}
</label>
<Field
id={id}
register={register(id, { required: true, pattern: validationPattern })}
className={`w-full p-2 ${errors[id] ? 'border border-red-500' : ''}`}
{...rest}
/>
{errorMessage && <span className="text-red-500 text-sm mt-1">{errorMessage}</span>}
</motion.div>
);
};
69 changes: 69 additions & 0 deletions src/components/BrlaComponents/BrlaSwapFields/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FC } from 'react';
import { AnimatePresence, motion, MotionProps } from 'motion/react';

import { OutputTokenType, OutputTokenTypes } from '../../../constants/tokenConfig';
import { BrlaField, StandardBrlaFieldOptions } from '../BrlaField';

interface BrlaSwapFieldsProps {
toToken: OutputTokenType;
}

const containerAnimation: MotionProps = {
initial: { opacity: 0, height: 0 },
animate: { opacity: 1, height: 'auto' },
exit: { opacity: 0, height: 0 },
transition: { duration: 0.3 },
};

const STANDARD_FIELDS = [
{ id: StandardBrlaFieldOptions.TAX_ID, label: 'Tax ID', index: 0 },
{ id: StandardBrlaFieldOptions.PIX_ID, label: 'PIX ID', index: 1 },
];

/**
* BrlaSwapFields component
*
* Renders PIX payment details form fields when Brazilian Real (BRL) is selected
* as the destination currency in the Swap form. Collects necessary information
* for processing PIX transfers to Brazilian bank accounts.
*/

export const BrlaSwapFields: FC<BrlaSwapFieldsProps> = ({ toToken }) => (
<AnimatePresence>
{toToken === OutputTokenTypes.BRL && (
<motion.div {...containerAnimation}>
<motion.h2
className="mb-2 text-2xl font-bold text-center text-blue-700"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.4,
delay: 0.1,
type: 'spring',
stiffness: 300,
damping: 15,
}}
>
PIX Details
</motion.h2>
<motion.p
className="text-gray-400 mb-4"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.4,
delay: 0.2,
type: 'spring',
stiffness: 300,
damping: 15,
}}
>
Which bank account should we send the funds to?
</motion.p>
{STANDARD_FIELDS.map((field) => (
<BrlaField key={field.id} id={field.id} label={field.label} index={field.index} />
))}
</motion.div>
)}
</AnimatePresence>
);
54 changes: 54 additions & 0 deletions src/components/BrlaComponents/KYCForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RefObject, useCallback } from 'react';
import { motion } from 'motion/react';
import { FormProvider, UseFormReturn } from 'react-hook-form';

import { FeeComparisonRef } from '../../FeeComparison';
import { BrlaField, BrlaFieldProps } from '../BrlaField';
import { KYCFormData } from '../../../hooks/brla/useKYCForm';

interface KYCFormProps {
fields: BrlaFieldProps[];
form: UseFormReturn<KYCFormData>;
onSubmit: (formData: KYCFormData) => Promise<void>;
onBackClick: () => void;
feeComparisonRef: RefObject<FeeComparisonRef | null>;
}

export const KYCForm = ({ form, onSubmit, onBackClick, fields, feeComparisonRef }: KYCFormProps) => {
const { handleSubmit } = form;

const compareFeesClick = useCallback(() => {
feeComparisonRef.current?.scrollIntoView();
}, [feeComparisonRef]);

return (
<FormProvider {...form}>
<motion.form
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.3 }}
className="px-4 pt-4 pb-2 mx-4 mt-8 mb-4 rounded-lg shadow-custom md:mx-auto md:w-96 min-h-[480px] flex flex-col"
onSubmit={handleSubmit(onSubmit)}
>
<h1 className="mt-2 mb-5 text-3xl font-bold text-center text-blue-700">KYC Details</h1>
{fields.map((field) => (
<BrlaField key={field.id} {...field} />
))}

<div className="grid gap-3 mt-8 mb-12">
<div className="flex gap-3">
<button type="button" className="btn-vortex-primary-inverse btn flex-1" onClick={onBackClick}>
Back
</button>
<button type="submit" className="btn-vortex-primary btn flex-1">
Continue
</button>
</div>
<button type="button" className="btn-vortex-primary-inverse btn flex-1" onClick={compareFeesClick}>
Compare Fees
</button>
</div>
</motion.form>
</FormProvider>
);
};
Loading
Loading