Skip to content

Commit 73ccf09

Browse files
aguxezfranm91
andcommitted
✨ app: add card limit kyc flow
co-authored-by: franm <fran.marienhoff@gmail.com>
1 parent 31a15e7 commit 73ccf09

8 files changed

Lines changed: 168 additions & 50 deletions

File tree

src/components/card/SpendingLimits.tsx

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import { useTranslation } from "react-i18next";
33
import { Pressable } from "react-native";
44

55
import { Plus } from "@tamagui/lucide-icons";
6+
import { useToastController } from "@tamagui/toast";
67
import { ScrollView, XStack, YStack } from "tamagui";
78

9+
import { useQuery } from "@tanstack/react-query";
10+
811
import SpendingLimit from "./SpendingLimit";
912
import { newMessage } from "../../utils/intercom";
13+
import { startCardLimitKYC } from "../../utils/persona";
1014
import reportError from "../../utils/reportError";
15+
import InfoAlert from "../shared/InfoAlert";
1116
import ModalSheet from "../shared/ModalSheet";
1217
import SafeView from "../shared/SafeView";
1318
import Button from "../shared/StyledButton";
1419
import Text from "../shared/Text";
1520
import View from "../shared/View";
1621

22+
import type { KYCStatus } from "../../utils/server";
23+
1724
export default function SpendingLimits({
1825
open,
1926
onClose,
@@ -26,6 +33,10 @@ export default function SpendingLimits({
2633
totalSpent: number;
2734
}) {
2835
const { t } = useTranslation();
36+
const toast = useToastController();
37+
const { data: cardLimitStatus } = useQuery<KYCStatus>({ queryKey: ["kyc", "cardLimit"], enabled: limit != null });
38+
const processing = cardLimitStatus?.code === "processing";
39+
const alreadyApproved = cardLimitStatus?.code === "ok";
2940
return (
3041
<ModalSheet open={open} onClose={onClose}>
3142
<SafeView paddingTop={0} fullScreen borderTopLeftRadius="$r4" borderTopRightRadius="$r4">
@@ -44,17 +55,42 @@ export default function SpendingLimits({
4455
<YStack paddingBottom="$s4">
4556
<SpendingLimit title={t("Weekly")} limit={limit} totalSpent={totalSpent} />
4657
</YStack>
47-
<Button
48-
onPress={() => {
49-
newMessage(t("I want to increase my spending limit")).catch(reportError);
50-
}}
51-
primary
52-
>
53-
<Button.Text>{t("Increase spending limit")}</Button.Text>
54-
<Button.Icon>
55-
<Plus />
56-
</Button.Icon>
57-
</Button>
58+
{processing ? (
59+
<InfoAlert
60+
title={t(
61+
"Your limit increase request is under review. We'll let you know once it's been processed.",
62+
)}
63+
/>
64+
) : (
65+
<Button
66+
onPress={() => {
67+
onClose();
68+
if (alreadyApproved) newMessage(t("I want to increase my spending limit")).catch(reportError);
69+
else
70+
startCardLimitKYC()
71+
.then((result) => {
72+
if (result.status === "error")
73+
toast.show(t("Something went wrong. Please try again."), {
74+
native: true,
75+
burntOptions: { haptic: "error", preset: "error" },
76+
});
77+
})
78+
.catch((error: unknown) => {
79+
reportError(error);
80+
toast.show(t("Something went wrong. Please try again."), {
81+
native: true,
82+
burntOptions: { haptic: "error", preset: "error" },
83+
});
84+
});
85+
}}
86+
primary
87+
>
88+
<Button.Text>{t("Increase spending limit")}</Button.Text>
89+
<Button.Icon>
90+
<Plus />
91+
</Button.Icon>
92+
</Button>
93+
)}
5894
<XStack alignSelf="center">
5995
<Pressable onPress={onClose} hitSlop={20}>
6096
<Text emphasized footnote color="$interactiveTextBrandDefault">

src/components/home/Home.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import PortfolioSummary from "./PortfolioSummary";
3333
import SpendingLimitSheet from "./SpendingLimitSheet";
3434
import VisaSignatureBanner from "./VisaSignatureBanner";
3535
import VisaSignatureModal from "./VisaSignatureSheet";
36+
import { newMessage } from "../../utils/intercom";
37+
import { startCardLimitKYC } from "../../utils/persona";
3638
import queryClient from "../../utils/queryClient";
3739
import reportError from "../../utils/reportError";
3840
import { cardModeMutationOptions } from "../../utils/server";
@@ -55,7 +57,7 @@ import SafeView from "../shared/SafeView";
5557
import View from "../shared/View";
5658

5759
import type { ActivityItem } from "../../utils/queryClient";
58-
import type { CardDetails, KYCStatus } from "../../utils/server";
60+
import type { CardActivity, CardDetails, KYCStatus } from "../../utils/server";
5961
import type { Credential } from "@exactly/common/validation";
6062

6163
const HEALTH_FACTOR_THRESHOLD = (WAD * 11n) / 10n;
@@ -131,6 +133,20 @@ export default function Home() {
131133
kycStatus && "code" in kycStatus && (kycStatus.code === "ok" || kycStatus.code === "legacy kyc"),
132134
);
133135
const { data: card } = useQuery<CardDetails>({ queryKey: ["card", "details"], enabled: !!account && !!bytecode });
136+
const { data: cardActivity } = useQuery<CardActivity[]>({ queryKey: ["activity", "card"] });
137+
const { data: cardLimitStatus } = useQuery<KYCStatus>({ queryKey: ["kyc", "cardLimit"], enabled: !!card });
138+
const cardLimitProcessing = cardLimitStatus?.code === "processing";
139+
const cardLimitApproved = cardLimitStatus?.code === "ok";
140+
const spendingLimitReached = useMemo(() => {
141+
if (!card?.limit.amount || !cardActivity) return false;
142+
const limit = card.limit.amount / 100;
143+
const totalSpent = cardActivity.reduce((sum, item) => {
144+
if (item.type !== "panda") return sum;
145+
const elapsed = (Date.now() - new Date(item.timestamp).getTime()) / 1000;
146+
return elapsed <= 604_800 ? sum + item.usdAmount : sum;
147+
}, 0);
148+
return totalSpent / limit >= 0.9;
149+
}, [card?.limit.amount, cardActivity]);
134150
const { data: spotlightShown } = useQuery<boolean>({ queryKey: ["settings", "installments-spotlight"] });
135151
const toast = useToastController();
136152
const { mutate: mutateMode } = useMutation({
@@ -207,6 +223,32 @@ export default function Home() {
207223
<View flex={1} gap="$s5" paddingBottom="$s5">
208224
<YStack backgroundColor="$backgroundSoft" padding="$s4" gap="$s4">
209225
{markets && healthFactor(markets) < HEALTH_FACTOR_THRESHOLD && <LiquidationAlert />}
226+
{spendingLimitReached && !cardLimitProcessing && (
227+
<InfoAlert
228+
variant="warning"
229+
title={t("You've reached 90% of your weekly card spending limit.")}
230+
actionText={t("Increase spending limit")}
231+
onPress={() => {
232+
if (cardLimitApproved) newMessage(t("I want to increase my spending limit")).catch(reportError);
233+
else
234+
startCardLimitKYC()
235+
.then((result) => {
236+
if (result.status === "error")
237+
toast.show(t("Something went wrong. Please try again."), {
238+
native: true,
239+
burntOptions: { haptic: "error", preset: "error" },
240+
});
241+
})
242+
.catch((error: unknown) => {
243+
reportError(error);
244+
toast.show(t("Something went wrong. Please try again."), {
245+
native: true,
246+
burntOptions: { haptic: "error", preset: "error" },
247+
});
248+
});
249+
}}
250+
/>
251+
)}
210252
{(showKYCMigration || showPluginOutdated) && (
211253
<InfoAlert
212254
title={t(

src/components/shared/InfoAlert.tsx

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,56 @@
11
import React from "react";
22
import { Pressable } from "react-native";
33

4-
import { ChevronRight, Info } from "@tamagui/lucide-icons";
4+
import { AlertTriangle, ChevronRight, Info } from "@tamagui/lucide-icons";
55
import { Spinner, View, XStack } from "tamagui";
66

77
import Text from "./Text";
8+
9+
const variants = {
10+
info: {
11+
bg: "$interactiveBaseInformationSoftDefault",
12+
iconBg: "$interactiveBaseInformationDefault",
13+
icon: Info,
14+
color: "$interactiveOnBaseInformationDefault",
15+
text: "$interactiveOnBaseInformationSoft",
16+
},
17+
warning: {
18+
bg: "$interactiveBaseWarningSoftDefault",
19+
iconBg: "$interactiveBaseWarningDefault",
20+
icon: AlertTriangle,
21+
color: "$interactiveOnBaseWarningDefault",
22+
text: "$interactiveOnBaseWarningSoft",
23+
},
24+
error: {
25+
bg: "$interactiveBaseErrorSoftDefault",
26+
iconBg: "$interactiveBaseErrorDefault",
27+
icon: AlertTriangle,
28+
color: "$interactiveOnBaseErrorDefault",
29+
text: "$interactiveOnBaseErrorSoft",
30+
},
31+
} as const;
32+
833
export default function InfoAlert({
934
title,
1035
actionText,
1136
loading,
1237
onPress,
38+
variant = "info",
1339
}: {
1440
actionText?: string;
1541
loading?: boolean;
1642
onPress?: () => void;
1743
title: string;
44+
variant?: keyof typeof variants;
1845
}) {
46+
const { bg, iconBg, icon: Icon, color, text } = variants[variant];
1947
return (
20-
<XStack borderRadius="$r3" backgroundColor="$interactiveBaseInformationSoftDefault" overflow="hidden">
21-
<View
22-
padding="$s4"
23-
backgroundColor="$interactiveBaseInformationDefault"
24-
justifyContent="center"
25-
alignItems="center"
26-
alignSelf="stretch"
27-
>
28-
<Info size={32} color="$interactiveOnBaseInformationDefault" />
48+
<XStack borderRadius="$r3" backgroundColor={bg} overflow="hidden">
49+
<View padding="$s4" backgroundColor={iconBg} justifyContent="center" alignItems="center" alignSelf="stretch">
50+
<Icon size={32} color={color} />
2951
</View>
3052
<View gap="$s2" padding="$s4" flex={1}>
31-
<Text subHeadline color="$interactiveOnBaseInformationSoft">
53+
<Text subHeadline color={text}>
3254
{title}
3355
</Text>
3456
<Pressable
@@ -39,14 +61,10 @@ export default function InfoAlert({
3961
>
4062
{actionText && (
4163
<XStack gap="$s1" alignItems="center">
42-
<Text emphasized subHeadline color="$interactiveOnBaseInformationSoft">
64+
<Text emphasized subHeadline color={text}>
4365
{actionText}
4466
</Text>
45-
{loading ? (
46-
<Spinner color="$interactiveOnBaseInformationSoft" />
47-
) : (
48-
<ChevronRight size={16} color="$interactiveOnBaseInformationSoft" strokeWidth={3} />
49-
)}
67+
{loading ? <Spinner color={text} /> : <ChevronRight size={16} color={text} strokeWidth={3} />}
5068
</XStack>
5169
)}
5270
</Pressable>

src/i18n/es-AR.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@
119119
"You must repay each installment manually before its due date.": "Debés pagar cada cuota manualmente antes de su fecha de vencimiento.",
120120
"You send": "Enviás",
121121
"Your card is awaiting activation. Follow the steps to enable it.": "Tu tarjeta está a la espera de activación. Seguí los pasos para habilitarla.",
122+
"Your limit increase request is under review. We'll let you know once it's been processed.": "Tu solicitud de aumento de límite está en revisión. Te vamos a avisar cuando se haya procesado.",
123+
"You've reached 90% of your weekly card spending limit.": "Alcanzaste el 90% de tu límite de gasto semanal.",
122124
"Your password manager does not support passkey backups. Please try a different one": "Tu gestor de contraseñas no admite copias de seguridad de llaves de acceso. Por favor, probá con otro.",
123125
"Your spending limit is the maximum amount you can spend on your Exa Card.": "Tu límite de gasto es el monto máximo que podés gastar con tu Exa Card.",
124126
"Your transactions will show up here once you get started. Add funds to begin!": "Tus transacciones aparecerán aquí una vez que comiences. ¡Agregá fondos para comenzar!",

src/i18n/es.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,13 +653,15 @@
653653
"You send": "Envías",
654654
"You will pay": "Pagarás",
655655
"You’ll be able to add funds soon.": "Pronto podrás agregar fondos.",
656+
"You've reached 90% of your weekly card spending limit.": "Has alcanzado el 90% de tu límite de gasto semanal.",
656657
"You’re all caught up! Start using your card in Pay Later mode to see payments listed here.": "¡Estás al día! Empieza a usar tu tarjeta en modo Pagar Después para ver los pagos aquí.",
657658
"You’re all set!": "¡Todo listo!",
658659
"You’re trying to borrow more than your collateral allows. Please enter a lower amount.": "Estás intentando pedir prestado más de lo que tu garantía permite. Por favor, introduce un monto menor.",
659660
"Your {{chain}} address": "Tu dirección en {{chain}}",
660661
"Your address needs to be verified": "Tu dirección necesita ser verificada",
661662
"Your card is awaiting activation. Follow the steps to enable it.": "Tu tarjeta está a la espera de activación. Sigue los pasos para habilitarla.",
662663
"Your card’s PIN may be required to confirm transactions and ensure security.": "El PIN de tu tarjeta puede ser necesario para confirmar transacciones y garantizar la seguridad.",
664+
"Your limit increase request is under review. We’ll let you know once it’s been processed.": "Tu solicitud de aumento de límite está en revisión. Te avisaremos cuando se haya procesado.",
663665
"Your Exa account": "Tu cuenta Exa",
664666
"Your Exa Card is now upgraded to Visa Signature.": "Tu Exa Card ha sido actualizada a Visa Signature.",
665667
"Your funds serve as collateral to increase your spending limits.": "Tus fondos sirven como garantía para aumentar tus límites de gasto.",

src/i18n/pt.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,13 @@
660660
"Your address needs to be verified": "Seu endereço precisa ser verificado",
661661
"Your card is awaiting activation. Follow the steps to enable it.": "Seu cartão está aguardando ativação. Siga os passos para ativá-lo.",
662662
"Your card’s PIN may be required to confirm transactions and ensure security.": "O PIN do seu cartão pode ser necessário para confirmar transações e garantir a segurança.",
663+
"Your limit increase request is under review. We’ll let you know once it’s been processed.": "Sua solicitação de aumento de limite está em análise. Avisaremos quando for processada.",
663664
"Your Exa account": "Sua conta Exa",
664665
"Your Exa Card is now upgraded to Visa Signature.": "Seu Exa Card foi atualizado para Visa Signature.",
665666
"Your funds serve as collateral to increase your spending limits.": "Seus fundos servem como garantia para aumentar seus limites de gastos.",
666667
"Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Seus fundos servem como garantia, aumentando seus limites de gastos. Quanto mais fundos você adicionar, mais poderá gastar com o Exa Card.",
667668
"Your ID needs to be updated": "Seu documento precisa ser atualizado",
669+
"You've reached 90% of your weekly card spending limit.": "Você atingiu 90% do seu limite de gastos semanal.",
668670
"Your password manager does not support passkey backups. Please try a different one": "Seu gerenciador de senhas não suporta backup de chaves de acesso. Por favor, tente outro",
669671
"Your portfolio": "Seu portfólio",
670672
"Your Portfolio": "Seu portfólio",

0 commit comments

Comments
 (0)