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
145 changes: 145 additions & 0 deletions components/CardWaitlist/CardFeesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { View } from 'react-native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';

import AuthButton from '@/components/AuthButton';
import GetCardButton from '@/components/CardWaitlist/GetCardButton';
import SolidCardSummary from '@/components/CardWaitlist/SolidCardSummary';
import ResponsiveModal, { ModalState } from '@/components/ResponsiveModal';
import { Text } from '@/components/ui/text';
import { getAsset } from '@/lib/assets';

const MODAL_STATE: ModalState = { name: 'card-fees', number: 1 };
const CLOSE_STATE: ModalState = { name: 'close', number: 0 };

interface CardFeesModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}

type DetailItemProps = {
icon: ReturnType<typeof getAsset>;
title: string;
description: React.ReactNode;
};

const DetailItem = ({ icon, title, description }: DetailItemProps) => (
<View className="flex-row gap-3">
<View className="h-12 w-12 items-center justify-center rounded-full bg-[#94F27F26]">
<Image source={icon} style={{ width: 28, height: 28 }} contentFit="contain" />
</View>
<View className="flex-1 gap-1">
<Text className="text-base font-semibold">{title}</Text>
{typeof description === 'string' ? (
<Text className="text-sm leading-5 text-muted-foreground">{description}</Text>
) : (
description
)}
</View>
</View>
);

const CardFeesModal = ({ isOpen, onOpenChange }: CardFeesModalProps) => {
return (
<ResponsiveModal
currentModal={MODAL_STATE}
previousModal={CLOSE_STATE}
isOpen={isOpen}
onOpenChange={onOpenChange}
trigger={null}
title="Fees and charges"
titleClassName="items-center w-full"
contentKey="card-fees"
contentClassName="md:max-w-md"
shouldAnimate={false}
>
<View className="gap-6">
<LinearGradient
colors={['rgba(148, 242, 127, 0.25)', 'rgba(148, 242, 127, 0)']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{ borderRadius: 20, overflow: 'hidden' }}
>
<View className="relative p-5">
<View className="absolute right-0 top-0">
<Image
source={getAsset('images/cards.png')}
style={{ width: 140, height: 160 }}
contentFit="contain"
/>
</View>
<SolidCardSummary
compact
topUpLabel={'Zero top-up fee,\nzero monthly fee'}
/>
</View>
</LinearGradient>

<View className="gap-4">
<Text className="text-base font-semibold text-muted-foreground">More details</Text>
<View className="gap-5">
<DetailItem
icon={getAsset('images/dollar-yellow.png')}
title="No hidden fees"
description={
<View>
<Text className="text-sm leading-5 text-muted-foreground">
FX fee of just 1% on non-USD transactions
</Text>
<Text className="text-sm leading-5 text-muted-foreground">
No cross-border fees
</Text>
<Text className="text-sm leading-5 text-muted-foreground">
No international transaction fees
</Text>
</View>
}
/>
<DetailItem
icon={getAsset('images/dollar-yellow.png')}
title="Global acceptance"
description="Spend anywhere Visa is accepted"
/>
<DetailItem
icon={getAsset('images/dollar-yellow.png')}
title="Borrow agains savings"
description="Keep earning on your savings while spending"
/>
<DetailItem
icon={getAsset('images/card-safe.png')}
title="Safe by design"
description="Non-custodial, secured by passkeys"
/>
<DetailItem
icon={getAsset('images/card-effortless.png')}
title="Effortless setup"
description={
<View>
<Text className="text-sm leading-5 text-muted-foreground">
Start using instantly.
</Text>
<View className="flex-row items-center gap-1.5">
<Image
source={getAsset('images/apple-google-pay.png')}
alt="Apple/Google Pay"
style={{ width: 82, height: 19 }}
contentFit="contain"
/>
<Text className="text-sm leading-5 text-muted-foreground">support</Text>
</View>
</View>
}
/>
</View>
</View>

<AuthButton>
<GetCardButton className="w-full" />
</AuthButton>
</View>
</ResponsiveModal>
);
};

export default CardFeesModal;
25 changes: 2 additions & 23 deletions components/CardWaitlist/CardWaitlistHeaderTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
import { View } from 'react-native';

import { Text } from '@/components/ui/text';
import { useDimension } from '@/hooks/useDimension';

const CardWaitlistHeaderTitle = () => {
const { isScreenMedium } = useDimension();

return (
<View className="gap-3">
<Text className="text-3xl font-semibold">Card</Text>

{isScreenMedium ? (
<View className="gap-1">
<Text className="text-[1rem] leading-5 opacity-70">
Spend with Visa and earn 3% cashback on every purchase.
</Text>
<Text className="text-[1rem] leading-5 opacity-70">
Non-custodial, secure by design, and ready to use with Apple or Google Pay.
</Text>
</View>
) : (
<View className="max-w-xs">
<Text className="text-[1rem] text-sm font-medium leading-5 opacity-70">
Spend with Visa and earn 3% cashback on every purchase. Non-custodial, secure by design,
and ready to use with Apple or Google Pay.
</Text>
</View>
)}
<View>
<Text className="text-3xl font-semibold">Free Visa Vard</Text>
</View>
);
};
Expand Down
155 changes: 18 additions & 137 deletions components/CardWaitlist/CardWaitlistPage.tsx
Original file line number Diff line number Diff line change
@@ -1,141 +1,23 @@
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import React, { useState } from 'react';
import { Pressable, View } from 'react-native';
import { Image } from 'expo-image';
import { ChevronRight } from 'lucide-react-native';

import AuthButton from '@/components/AuthButton';
import CardFeesModal from '@/components/CardWaitlist/CardFeesModal';
import CardWaitlistContainer from '@/components/CardWaitlist/CardWaitlistContainer';
import CardWaitlistHeader from '@/components/CardWaitlist/CardWaitlistHeader';
import CardWaitlistHeaderButtons from '@/components/CardWaitlist/CardWaitlistHeaderButtons';
import CardWaitlistHeaderTitle from '@/components/CardWaitlist/CardWaitlistHeaderTitle';
import { CashbackIcon } from '@/components/CardWaitlist/CashbackIcon';
import GetCardButton from '@/components/CardWaitlist/GetCardButton';
import SolidCardSummary from '@/components/CardWaitlist/SolidCardSummary';
import { Text } from '@/components/ui/text';
import { useDimension } from '@/hooks/useDimension';
import useUser from '@/hooks/useUser';
import { getCashbackPercentage } from '@/lib/api';
import { getAsset } from '@/lib/assets';
import { cn } from '@/lib/utils';

type ClassNames = {
container?: string;
title?: string;
description?: string;
};

type FeatureProps = {
icon: number | React.ReactElement;
title: string;
description: string | React.ReactNode;
classNames?: ClassNames;
};

const Feature = ({ icon, title, description, classNames }: FeatureProps) => {
return (
<View className={cn('flex-row gap-2 md:w-[17rem] md:items-center', classNames?.container)}>
{React.isValidElement(icon) ? (
icon
) : (
<Image source={icon} style={{ width: 50, height: 50 }} contentFit="contain" />
)}
<View>
<Text className={cn('text-lg font-bold leading-5', classNames?.title)}>{title}</Text>
{typeof description === 'string' ? (
<Text
className={cn('max-w-48 text-muted-foreground md:max-w-56', classNames?.description)}
>
{description}
</Text>
) : (
description
)}
</View>
</View>
);
};

const getFeatures = (cashbackPercentage: number) => [
{
icon: getAsset('images/card-global.png'),
title: 'Global acceptance',
description: '200M+ Visa merchants',
classNames: {
container: 'items-center',
},
},
{
icon: <CashbackIcon percentage={cashbackPercentage} />,
title: 'Earn while you spend',
description: `${Math.round(cashbackPercentage * 100)}% cashback for every purchase`,
classNames: {
container: 'items-center',
description: 'max-w-full md:max-w-full',
},
},
{
icon: getAsset('images/card-safe.png'),
title: 'Secure by design',
description: 'Non-custodial, secured by passkeys',
},
{
icon: getAsset('images/card-effortless.png'),
title: 'Effortless setup',
description: (
<View>
<Text className="text-muted-foreground">Start using instantly</Text>
<View className="flex-row items-center gap-1.5">
<Image
source={getAsset('images/apple-google-pay.png')}
alt="Apple/Google Pay"
style={{ width: 82, height: 19 }}
contentFit="contain"
/>
<Text className="text-muted-foreground">support</Text>
</View>
</View>
),
},
];

const CardWaitlistPage = () => {
const { user } = useUser();
const [loading, setLoading] = useState(true);
const [cashbackPercentage, setCashbackPercentage] = useState<number>(0.03); // Default to 3%
const { isScreenMedium } = useDimension();

useEffect(() => {
const checkWaitlistStatus = async () => {
if (user?.email) {
try {
const [cashbackResponse] = await Promise.all([getCashbackPercentage()]);
setCashbackPercentage(cashbackResponse.percentage);
} catch (error) {
console.error('Error fetching cashback:', error);
}
}
setLoading(false);
};

checkWaitlistStatus();
}, [user?.email]);

if (loading) {
return (
<CardWaitlistHeader
content={
<View className="md:flex-row md:items-center md:justify-between">
<CardWaitlistHeaderTitle />
{isScreenMedium && <CardWaitlistHeaderButtons />}
</View>
}
>
<CardWaitlistContainer>
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#94F27F" />
</View>
</CardWaitlistContainer>
</CardWaitlistHeader>
);
}
const [feesOpen, setFeesOpen] = useState(false);

return (
<CardWaitlistHeader
Expand All @@ -147,18 +29,8 @@ const CardWaitlistPage = () => {
}
>
<CardWaitlistContainer>
<View className="flex-1 gap-8 bg-transparent p-5 py-7 pb-20 md:justify-center md:gap-14 md:px-12 md:py-10">
<View className="items-start gap-4">
<Text className="text-3.5xl font-semibold md:text-4.5xl">
Introducing the Solid Card
</Text>
</View>

<View className="max-w-2xl flex-row flex-wrap gap-10">
{getFeatures(cashbackPercentage).map(feature => (
<Feature key={feature.title} {...feature} />
))}
</View>
<View className="flex-1 gap-8 bg-transparent p-5 py-7 pb-20 md:justify-center md:gap-10 md:px-12 md:py-10">
<SolidCardSummary />

{!isScreenMedium && (
<Image
Expand All @@ -168,13 +40,22 @@ const CardWaitlistPage = () => {
/>
)}

<View className="md:items-start">
<View className="flex-row items-center gap-6">
<AuthButton>
<GetCardButton />
</AuthButton>
<Pressable
onPress={() => setFeesOpen(true)}
className="flex-row items-center gap-1 web:hover:opacity-70"
>
<Text className="text-base font-bold">Fees and charges</Text>
<ChevronRight size={16} color="white" />
</Pressable>
</View>
</View>
</CardWaitlistContainer>

<CardFeesModal isOpen={feesOpen} onOpenChange={setFeesOpen} />
</CardWaitlistHeader>
);
};
Expand Down
9 changes: 7 additions & 2 deletions components/CardWaitlist/GetCardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import { Text } from '@/components/ui/text';
import { path } from '@/constants/path';
import { TRACKING_EVENTS } from '@/constants/tracking-events';
import { track } from '@/lib/analytics';
import { cn } from '@/lib/utils';

const GetCardButton = () => {
interface GetCardButtonProps {
className?: string;
}

const GetCardButton = ({ className }: GetCardButtonProps) => {
const router = useRouter();

const handleGetCard = async () => {
Expand All @@ -18,7 +23,7 @@ const GetCardButton = () => {
};

return (
<Button variant="brand" className="h-12 rounded-xl px-8" onPress={handleGetCard}>
<Button variant="brand" className={cn('h-12 rounded-xl px-8', className)} onPress={handleGetCard}>
<Text className="text-base font-bold">Get your card</Text>
</Button>
);
Expand Down
Loading
Loading