Skip to content
Open
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
5 changes: 3 additions & 2 deletions apps/frontend/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
--radius: 0.625rem;

/* QuoteSummary layout */
--quote-summary-height: 70px;
--quote-summary-height: 88px;
--widget-min-height: 506px;
}

@layer base {
Expand Down Expand Up @@ -249,7 +250,7 @@

@layer utilities {
.bottom-above-quote {
bottom: calc(var(--quote-summary-height) + 2rem);
bottom: calc(var(--quote-summary-height) + 1rem);
}

.shadow-custom {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid";
import { Trans, useTranslation } from "react-i18next";
import { useQuote } from "../../../stores/quote/useQuoteStore";
import { QuoteSummary } from "../../QuoteSummary";
import { StepFooter } from "../../StepFooter";

interface AveniaKYBVerifyStepProps {
titleKey: string;
Expand Down Expand Up @@ -32,7 +32,7 @@ export const AveniaKYBVerifyStep = ({
const { t } = useTranslation();

return (
<div className="relative flex min-h-[506px] w-full grow flex-col">
<div className="relative flex min-h-(--widget-min-height) w-full grow flex-col">
<div className="flex-1 pb-36">
<div className="mt-8 mb-4 flex w-full flex-col">
<div>
Expand Down Expand Up @@ -79,7 +79,7 @@ export const AveniaKYBVerifyStep = ({
</div>
</div>

<div className="absolute right-0 bottom-above-quote left-0 z-[5] mb-4">
<StepFooter quote={quote}>
<div className="mt-8 flex gap-4">
<button className="btn-vortex-primary-inverse btn flex-1" onClick={onCancel}>
{t(cancelButtonKey)}
Expand All @@ -102,8 +102,7 @@ export const AveniaKYBVerifyStep = ({
</a>
)}
</div>
</div>
{quote && <QuoteSummary quote={quote} />}
</StepFooter>
</div>
);
};
4 changes: 2 additions & 2 deletions apps/frontend/src/components/Avenia/AveniaKYBForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export const AveniaKYBForm = () => {
];

return (
<div className="relative flex h-full grow flex-col">
<div className="flex flex-col flex-1">
<div className="relative flex min-h-(--widget-min-height) grow flex-col">
<div className="flex flex-1 flex-col">
<AveniaVerificationForm aveniaKycActor={aveniaKycActor} fields={companyFormFields} form={kycForm} isCompany={true} />
</div>
{quote && <QuoteSummary quote={quote} />}
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/components/Avenia/AveniaKYCForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ export const AveniaKYCForm = () => {
}

return (
<div className="relative flex h-full grow flex-col">
<div className="flex flex-col flex-1">
<div className="relative flex min-h-(--widget-min-height) grow flex-col">
<div className="flex flex-1 flex-col">
<div className="relative">
<div className="mb-4">
<StepBackButton />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Trans, useTranslation } from "react-i18next";
import { KYCFormData } from "../../../hooks/brla/useKYCForm";
import { useMaintenanceAwareButton } from "../../../hooks/useMaintenanceAware";
import { AveniaKycActorRef } from "../../../machines/types";

import { StepFooter } from "../../StepFooter";
import { AveniaField, AveniaFieldProps, ExtendedAveniaFieldOptions } from "../AveniaField";

interface AveniaVerificationFormProps {
Expand All @@ -31,7 +33,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany
<FormProvider {...form}>
<motion.form
animate={{ opacity: 1, scale: 1 }}
className="mt-8 mb-4 flex h-full min-h-[506px] w-full flex-col"
className="mt-8 mb-4 flex w-full flex-col"
initial={{ opacity: 0.8, scale: 0.9 }}
onSubmit={handleSubmit(onSubmit)}
transition={{ duration: 0.3 }}
Expand Down Expand Up @@ -59,7 +61,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany
))}
</div>
{!isCompany && (
<div className="mt-4 text-center text-primary-500">
<div className="my-4 text-balance text-primary-500 text-sm">
<Trans
components={{
a: <a className="underline" href="https://www.avenia.io" rel="noreferrer" target="_blank" />
Expand All @@ -75,26 +77,24 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany
</div>
)}
</div>
<div className="absolute right-0 bottom-above-quote left-0 z-[5] mb-4">
<div className="mt-8 grid gap-3">
<button
className="btn-vortex-primary btn w-full"
disabled={isMaintenanceDisabled || buttonProps.disabled || isFormInvalid}
onClick={() => {
const formData = form.getValues();
aveniaKycActor.send({ formData, type: "FORM_SUBMIT" });
}}
title={buttonProps.title}
type="button"
>
{isMaintenanceDisabled
? buttonProps.title
: isCompany
? t("components.aveniaKYB.buttons.next")
: t("components.aveniaKYC.buttons.next")}
</button>
</div>
</div>
<StepFooter aboveQuote>
<button
className="btn-vortex-primary btn w-full"
disabled={isMaintenanceDisabled || buttonProps.disabled || isFormInvalid}
onClick={() => {
const formData = form.getValues();
aveniaKycActor.send({ formData, type: "FORM_SUBMIT" });
}}
title={buttonProps.title}
type="button"
>
{isMaintenanceDisabled
? buttonProps.title
: isCompany
? t("components.aveniaKYB.buttons.next")
: t("components.aveniaKYC.buttons.next")}
</button>
</StepFooter>
</motion.form>
</FormProvider>
);
Expand Down
142 changes: 97 additions & 45 deletions apps/frontend/src/components/Avenia/DocumentUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { CameraIcon, CheckCircleIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import { AveniaDocumentType } from "@vortexfi/shared";
import { motion } from "motion/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { durations, easings } from "../../../constants/animations";
import { useMaintenanceAwareButton } from "../../../hooks/useMaintenanceAware";
import { AveniaKycActorRef } from "../../../machines/types";
import { BrlaService } from "../../../services/api";
import { KycLevel2Toggle } from "../../KycLevel2Toggle";
import { StepFooter } from "../../StepFooter";

const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15 MB
const ALLOWED_TYPES = ["image/png", "image/jpeg", "application/pdf"];
Expand Down Expand Up @@ -36,6 +39,7 @@ async function uploadFileAsBuffer(file: File, url: string) {
export const DocumentUpload: React.FC<DocumentUploadProps> = ({ aveniaKycActor, taxId }) => {
const { t } = useTranslation();
const { buttonProps, isMaintenanceDisabled } = useMaintenanceAwareButton();
const shouldReduceMotion = useReducedMotion();

const [docType, setDocType] = useState<AveniaDocumentType>(AveniaDocumentType.DRIVERS_LICENSE);

Expand Down Expand Up @@ -169,68 +173,116 @@ export const DocumentUpload: React.FC<DocumentUploadProps> = ({ aveniaKycActor,
<span className="mb-1 text-gray-600">{label}</span>
<span className="text-gray-400 text-sm">{fileName || t("components.documentUpload.helperText")}</span>
<input accept=".png,.jpeg,.jpg,.pdf" className="hidden" onChange={onChange} type="file" />
{valid && <CheckCircleIcon className="absolute top-2 right-2 h-6 w-6 text-green-500" />}
<AnimatePresence>
{valid && (
<motion.div
animate={{ scale: 1 }}
className="absolute top-2 right-2"
exit={{ scale: 0 }}
initial={{ scale: 0 }}
transition={shouldReduceMotion ? { duration: 0 } : { damping: 15, stiffness: 200, type: "spring" }}
>
<CheckCircleIcon className="h-6 w-6 text-green-500" />
</motion.div>
)}
</AnimatePresence>
</label>
);

const fieldTransition = shouldReduceMotion ? { duration: 0 } : { duration: durations.normal, ease: easings.easeOutCubic };

return (
<motion.div
<motion.form
animate={{ opacity: 1, scale: 1 }}
className="mx-4 mt-8 mb-4 min-h-[506px] px-4 pt-6 pb-8 md:mx-auto"
className="mt-8 mb-4 flex w-full flex-col"
initial={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.3 }}
onSubmit={e => {
e.preventDefault();
handleSubmit();
}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: durations.slow, ease: easings.easeOutCubic }}
>
<h2 className="mb-6 text-center font-semibold text-2xl text-blue-700">{t("components.documentUpload.title")}</h2>
<p className="mb-4 text-center text-gray-600">{t("components.documentUpload.description")}</p>

<KycLevel2Toggle activeDocType={docType} onToggle={setDocType} />

<div className="grid grid-cols-1 gap-4">
{docType === AveniaDocumentType.ID && (
<>
{renderField(
t("components.documentUpload.fields.rgFront"),
e => handleFileChange(e, setFront, setFrontValid),
frontValid,
DocumentTextIcon,
front?.name
)}
{renderField(
t("components.documentUpload.fields.rgBack"),
e => handleFileChange(e, setBack, setBackValid),
backValid,
DocumentTextIcon,
back?.name
<div className="flex-1 pb-36">
<h2 className="mb-6 text-center font-semibold text-2xl text-blue-700">{t("components.documentUpload.title")}</h2>
<p className="mb-4 text-center text-gray-600">{t("components.documentUpload.description")}</p>

<KycLevel2Toggle activeDocType={docType} onToggle={setDocType} />

<div className="grid grid-cols-1 gap-4">
<AnimatePresence mode="wait">
{docType === AveniaDocumentType.ID ? (
<motion.div
animate={{ opacity: 1, y: 0 }}
className="grid grid-cols-1 gap-4"
exit={{ opacity: 0, y: -10 }}
initial={{ opacity: 0, y: 10 }}
key="id-fields"
transition={fieldTransition}
>
{renderField(
t("components.documentUpload.fields.rgFront"),
e => handleFileChange(e, setFront, setFrontValid),
frontValid,
DocumentTextIcon,
front?.name
)}
{renderField(
t("components.documentUpload.fields.rgBack"),
e => handleFileChange(e, setBack, setBackValid),
backValid,
DocumentTextIcon,
back?.name
)}
</motion.div>
) : (
<motion.div
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
initial={{ opacity: 0, y: 10 }}
key="cnh-fields"
transition={fieldTransition}
>
{renderField(
t("components.documentUpload.fields.cnhDocument"),
e => handleFileChange(e, setFront, setFrontValid),
frontValid,
DocumentTextIcon,
front?.name
)}
</motion.div>
)}
</>
)}
{docType === AveniaDocumentType.DRIVERS_LICENSE &&
renderField(
t("components.documentUpload.fields.cnhDocument"),
e => handleFileChange(e, setFront, setFrontValid),
frontValid,
DocumentTextIcon,
front?.name
</AnimatePresence>
</div>

<AnimatePresence>
{error && (
<motion.p
animate={{ opacity: 1 }}
className="mt-4 text-center text-red-800"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: durations.fast }}
>
{error}
</motion.p>
)}
</AnimatePresence>
</div>

{error && <p className="mt-4 text-center text-red-800">{error}</p>}

<div className="mt-8">
<StepFooter aboveQuote>
<button
className="btn-vortex-primary btn w-full"
onClick={handleSubmit}
type="button"
{...buttonProps}
disabled={buttonProps.disabled || isSubmitDisabled}
disabled={isMaintenanceDisabled || buttonProps.disabled || isSubmitDisabled}
title={buttonProps.title}
type="submit"
>
{isMaintenanceDisabled
? buttonProps.title
: loading
? t("components.documentUpload.buttons.uploading")
: t("components.documentUpload.buttons.finish")}
</button>
</div>
</motion.div>
</StepFooter>
</motion.form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { motion } from "motion/react";
import React from "react";
import { useTranslation } from "react-i18next";
import { AveniaKycActorRef, SelectedAveniaData } from "../../../machines/types";

import { KycStatus } from "../../../services/signingService";
import { Spinner } from "../../Spinner";

Expand All @@ -16,7 +17,7 @@ export const VerificationStatus: React.FC<VerificationStatusProps> = ({ aveniaKy
return (
<motion.div
animate={{ opacity: 1, scale: 1 }}
className="mx-4 mt-8 mb-4 flex min-h-[506px] flex-col items-center justify-center px-4 py-4 md:mx-auto"
className="mx-4 mt-8 mb-4 flex min-h-(--widget-min-height) flex-col items-center justify-center px-4 py-4 md:mx-auto"
initial={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
>
Expand Down
11 changes: 8 additions & 3 deletions apps/frontend/src/components/CollapsibleCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { createContext, forwardRef, ReactNode, useContext, useId, useState } from "react";
import { durations, easings } from "../../constants/animations";
import { cn } from "../../helpers/cn";

interface CollapsibleCardProps {
children: ReactNode;
Expand Down Expand Up @@ -71,8 +72,12 @@ const CollapsibleDetails = ({ children, className = "" }: CollapsibleDetailsProp

return (
<div
className={`grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none ${className}`}
style={{ gridTemplateRows: isExpanded ? "1fr" : "0fr" }}
className={cn(
"grid",
isExpanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
"transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none",
className
)}
>
<div className="overflow-hidden">
<AnimatePresence>
Expand All @@ -83,7 +88,7 @@ const CollapsibleDetails = ({ children, className = "" }: CollapsibleDetailsProp
exit={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 10 }}
id={detailsId}
initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 10 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: durations.slow, ease: easings.easeOutCubic }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: durations.normal, ease: easings.easeOutCubic }}
>
{children}
</motion.div>
Expand Down
Loading