diff --git a/app/bills/pay/[billerId]/page.tsx b/app/bills/pay/[billerId]/page.tsx index 18a54be..21fcaf2 100644 --- a/app/bills/pay/[billerId]/page.tsx +++ b/app/bills/pay/[billerId]/page.tsx @@ -1,7 +1,6 @@ 'use client' import { use } from 'react' -import { useRouter } from 'next/navigation' import { motion } from 'framer-motion' import { ArrowLeft, ShieldCheck, Zap } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -14,7 +13,6 @@ interface PageProps { } export default function BillerPaymentPage({ params }: PageProps) { - const router = useRouter() const { billerId } = use(params) const schema = BILLER_SCHEMAS[billerId] diff --git a/app/feature-highlights/page.tsx b/app/feature-highlights/page.tsx new file mode 100644 index 0000000..65ecd52 --- /dev/null +++ b/app/feature-highlights/page.tsx @@ -0,0 +1,16 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { FeatureHighlightsCarousel } from '@/components/onboarding/feature-highlights-carousel' + +export default function FeatureHighlightsPage() { + const router = useRouter() + + return ( + router.push('/welcome')} + onComplete={() => router.push('/wallet-setup')} + onSkip={() => router.push('/wallet-setup')} + /> + ) +} diff --git a/app/wallet-setup/page.tsx b/app/wallet-setup/page.tsx new file mode 100644 index 0000000..a668424 --- /dev/null +++ b/app/wallet-setup/page.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link' +import { ArrowLeft, Shield } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function WalletSetupPage() { + return ( +
+
+ +
+ +
+
+ + Wallet Setup +
+

+ Secure your wallet in 3 quick steps +

+

+ Your feature highlights are complete. Next, we will generate and verify your recovery + phrase so your funds stay protected. +

+
+ +
+ +
+
+ ) +} diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx new file mode 100644 index 0000000..87d0243 --- /dev/null +++ b/app/welcome/page.tsx @@ -0,0 +1,55 @@ +import Link from 'next/link' +import { ArrowRight, ShieldCheck } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function WelcomePage() { + return ( +
+
+ +
+
+ FINANCE REDEFINED +
+ +
+
+
+
+
+ +
+
+
+ +

Aframp

+

+ Welcome to the future of finance +

+

+ Securely manage your digital assets, trade with confidence, and build your wealth with + Aframp. +

+ +
+ + +
+
+
+ ) +} diff --git a/components/bills/payment-form.tsx b/components/bills/payment-form.tsx index 466c0ba..323a028 100644 --- a/components/bills/payment-form.tsx +++ b/components/bills/payment-form.tsx @@ -82,7 +82,7 @@ export function PaymentForm({ schema }: PaymentFormProps) { useEffect(() => { if (accountValue && accountValue.length >= 10 && !errors[schema.fields[0].name]) { const delayDebounceFn = setTimeout(() => { - validateAccount(accountValue) + validateAccount() }, 1000) return () => clearTimeout(delayDebounceFn) } else { @@ -90,7 +90,7 @@ export function PaymentForm({ schema }: PaymentFormProps) { } }, [accountValue, errors[schema.fields[0].name]]) - const validateAccount = async (value: string) => { + const validateAccount = async () => { setIsValidating(true) // Simulate API call await new Promise((resolve) => setTimeout(resolve, 1500)) @@ -101,7 +101,7 @@ export function PaymentForm({ schema }: PaymentFormProps) { setValidatedAccount(mockNames[Math.floor(Math.random() * mockNames.length)]) } - const onSubmit = async (data: FormValues) => { + const onSubmit = async (_data: FormValues) => { setIsProcessing(true) // Simulate payment processing await new Promise((resolve) => setTimeout(resolve, 3000)) @@ -255,7 +255,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {

- By clicking "Pay Now", you agree to our Terms of Service and acknowledge that this transaction is final. + By clicking "Pay Now", you agree to our Terms of Service and acknowledge that this transaction is final.

) diff --git a/components/bills/recent-billers.tsx b/components/bills/recent-billers.tsx index c315b1f..7dbe55b 100644 --- a/components/bills/recent-billers.tsx +++ b/components/bills/recent-billers.tsx @@ -5,7 +5,6 @@ import { motion } from 'framer-motion' import Link from 'next/link' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' import { Clock, Star } from 'lucide-react' interface Biller { diff --git a/components/onboarding/feature-highlights-carousel.tsx b/components/onboarding/feature-highlights-carousel.tsx new file mode 100644 index 0000000..52de269 --- /dev/null +++ b/components/onboarding/feature-highlights-carousel.tsx @@ -0,0 +1,294 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react' +import useEmblaCarousel from 'embla-carousel-react' +import { ArrowLeft, ArrowRight, Globe2, ShieldCheck, Sparkles, Zap } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +export type FeatureHighlightIllustration = 'security' | 'ease' | 'freedom' | 'network' + +export interface FeatureHighlightSlide { + id: string + title: string + description: string + illustration: FeatureHighlightIllustration +} + +interface FeatureHighlightsCarouselProps { + slides?: FeatureHighlightSlide[] + onComplete: () => void + onSkip?: () => void + onBack?: () => void + autoAdvanceMs?: number + className?: string +} + +const DEFAULT_SLIDES: FeatureHighlightSlide[] = [ + { + id: 'security', + title: 'Secure Digital Wallet', + description: + 'Experience military-grade protection with encrypted storage, multi-factor checks, and recovery safeguards.', + illustration: 'security', + }, + { + id: 'ease', + title: 'Built for Everyday Ease', + description: + 'Move from sign-up to your first transaction in minutes with guided flows and familiar actions.', + illustration: 'ease', + }, + { + id: 'freedom', + title: 'Trade with Total Freedom', + description: + 'Buy, sell, and move assets across borders with low friction and real-time settlement confidence.', + illustration: 'freedom', + }, + { + id: 'network', + title: 'Connected Financial Network', + description: + 'Link local money rails with global digital assets through a trusted, always-on payment network.', + illustration: 'network', + }, +] + +function Illustration({ variant }: { variant: FeatureHighlightIllustration }) { + const iconByVariant = { + security: ShieldCheck, + ease: Sparkles, + freedom: ArrowRight, + network: Globe2, + } as const + + const Icon = iconByVariant[variant] + + return ( +
+
+
+
+
+ + {variant === 'network' && ( +
+
+
+
+
+
+
+
+ )} + + {variant === 'freedom' && ( +
+ +
+ +
+ )} + +
+ +
+
+ ) +} + +export function FeatureHighlightsCarousel({ + slides = DEFAULT_SLIDES, + onComplete, + onSkip, + onBack, + autoAdvanceMs = 0, + className, +}: FeatureHighlightsCarouselProps) { + const [emblaRef, emblaApi] = useEmblaCarousel({ align: 'start', loop: false, dragFree: false }) + const [selectedIndex, setSelectedIndex] = useState(0) + + const slideCount = slides.length + const isLastSlide = selectedIndex === slideCount - 1 + const currentSlide = slides[selectedIndex] + + const scrollPrev = useCallback(() => { + emblaApi?.scrollPrev() + }, [emblaApi]) + + const scrollNext = useCallback(() => { + emblaApi?.scrollNext() + }, [emblaApi]) + + const scrollTo = useCallback( + (index: number) => { + emblaApi?.scrollTo(index) + }, + [emblaApi] + ) + + const selectSlide = useCallback(() => { + if (!emblaApi) return + setSelectedIndex(emblaApi.selectedScrollSnap()) + }, [emblaApi]) + + useEffect(() => { + if (!emblaApi) return + emblaApi.on('select', selectSlide) + emblaApi.on('reInit', selectSlide) + return () => { + emblaApi.off('select', selectSlide) + emblaApi.off('reInit', selectSlide) + } + }, [emblaApi, selectSlide]) + + useEffect(() => { + if (!emblaApi || autoAdvanceMs <= 0) return + const timer = window.setInterval(() => { + if (emblaApi.selectedScrollSnap() >= slideCount - 1) { + window.clearInterval(timer) + return + } + emblaApi.scrollNext() + }, autoAdvanceMs) + return () => window.clearInterval(timer) + }, [autoAdvanceMs, emblaApi, slideCount]) + + const handleBack = useCallback(() => { + if (selectedIndex > 0) { + scrollPrev() + return + } + onBack?.() + }, [onBack, scrollPrev, selectedIndex]) + + const handleNext = useCallback(() => { + if (isLastSlide) { + onComplete() + return + } + scrollNext() + }, [isLastSlide, onComplete, scrollNext]) + + const handleKeyboard = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault() + handleBack() + } + if (event.key === 'ArrowRight') { + event.preventDefault() + handleNext() + } + if (event.key === 'Home') { + event.preventDefault() + scrollTo(0) + } + if (event.key === 'End') { + event.preventDefault() + scrollTo(slideCount - 1) + } + }, + [handleBack, handleNext, scrollTo, slideCount] + ) + + const srProgressLabel = useMemo( + () => `Slide ${selectedIndex + 1} of ${slideCount}: ${currentSlide?.title ?? ''}`, + [currentSlide?.title, selectedIndex, slideCount] + ) + + return ( +
+
+ +

Feature Highlights

+
+
+ +
+
+ {slides.map((slide, index) => ( +
+ +
+

+ {slide.title} +

+

{slide.description}

+
+
+ ))} +
+
+ +
+
+ {slides.map((slide, index) => ( +
+ + + + +
+ +

+ {srProgressLabel} +

+
+ ) +} + +export { DEFAULT_SLIDES } diff --git a/lib/biller-schemas.ts b/lib/biller-schemas.ts index 09c7511..5bce597 100644 --- a/lib/biller-schemas.ts +++ b/lib/biller-schemas.ts @@ -1,5 +1,3 @@ -import { z } from 'zod' - export interface BillerField { id: string name: string diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..86e9f32 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited diff --git a/package.json b/package.json index b66026f..ad7cd36 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "next build", "dev": "next dev", - "lint": "next lint", + "lint": "eslint .", "start": "next start", "test": "jest", "test:watch": "jest --watch",