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
58 changes: 47 additions & 11 deletions src/components/card/SpendingLimits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import { useTranslation } from "react-i18next";
import { Pressable } from "react-native";

import { Plus } from "@tamagui/lucide-icons";
import { useToastController } from "@tamagui/toast";
import { ScrollView, XStack, YStack } from "tamagui";

import { useQuery } from "@tanstack/react-query";

import SpendingLimit from "./SpendingLimit";
import { newMessage } from "../../utils/intercom";
import { startCardLimitKYC } from "../../utils/persona";
import reportError from "../../utils/reportError";
import InfoAlert from "../shared/InfoAlert";
import ModalSheet from "../shared/ModalSheet";
import SafeView from "../shared/SafeView";
import Button from "../shared/StyledButton";
import Text from "../shared/Text";
import View from "../shared/View";

import type { KYCStatus } from "../../utils/server";

export default function SpendingLimits({
open,
onClose,
Expand All @@ -26,6 +33,10 @@ export default function SpendingLimits({
totalSpent: number;
}) {
const { t } = useTranslation();
const toast = useToastController();
const { data: cardLimitStatus } = useQuery<KYCStatus>({ queryKey: ["kyc", "cardLimit"], enabled: limit != null });
const processing = cardLimitStatus?.code === "processing";
const alreadyApproved = cardLimitStatus?.code === "ok";
return (
<ModalSheet open={open} onClose={onClose}>
<SafeView paddingTop={0} fullScreen borderTopLeftRadius="$r4" borderTopRightRadius="$r4">
Expand All @@ -44,17 +55,42 @@ export default function SpendingLimits({
<YStack paddingBottom="$s4">
<SpendingLimit title={t("Weekly")} limit={limit} totalSpent={totalSpent} />
</YStack>
<Button
onPress={() => {
newMessage(t("I want to increase my spending limit")).catch(reportError);
}}
primary
>
<Button.Text>{t("Increase spending limit")}</Button.Text>
<Button.Icon>
<Plus />
</Button.Icon>
</Button>
{processing ? (
<InfoAlert
title={t(
"Your limit increase request is under review. We'll let you know once it's been processed.",
)}
/>
) : (
<Button
onPress={() => {
onClose();
if (alreadyApproved) newMessage(t("I want to increase my spending limit")).catch(reportError);
else
startCardLimitKYC()
.then((result) => {
if (result.status === "error")
toast.show(t("Something went wrong. Please try again."), {
native: true,
burntOptions: { haptic: "error", preset: "error" },
});
})
.catch((error: unknown) => {
reportError(error);
toast.show(t("Something went wrong. Please try again."), {
native: true,
burntOptions: { haptic: "error", preset: "error" },
});
});
}}
primary
>
<Button.Text>{t("Increase spending limit")}</Button.Text>
<Button.Icon>
<Plus />
</Button.Icon>
</Button>
)}
<XStack alignSelf="center">
<Pressable onPress={onClose} hitSlop={20}>
<Text emphasized footnote color="$interactiveTextBrandDefault">
Expand Down
44 changes: 43 additions & 1 deletion src/components/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import PortfolioSummary from "./PortfolioSummary";
import SpendingLimitSheet from "./SpendingLimitSheet";
import VisaSignatureBanner from "./VisaSignatureBanner";
import VisaSignatureModal from "./VisaSignatureSheet";
import { newMessage } from "../../utils/intercom";
import { startCardLimitKYC } from "../../utils/persona";
import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
import { cardModeMutationOptions } from "../../utils/server";
Expand All @@ -55,7 +57,7 @@ import SafeView from "../shared/SafeView";
import View from "../shared/View";

import type { ActivityItem } from "../../utils/queryClient";
import type { CardDetails, KYCStatus } from "../../utils/server";
import type { CardActivity, CardDetails, KYCStatus } from "../../utils/server";
import type { Credential } from "@exactly/common/validation";

const HEALTH_FACTOR_THRESHOLD = (WAD * 11n) / 10n;
Expand Down Expand Up @@ -131,6 +133,20 @@ export default function Home() {
kycStatus && "code" in kycStatus && (kycStatus.code === "ok" || kycStatus.code === "legacy kyc"),
);
const { data: card } = useQuery<CardDetails>({ queryKey: ["card", "details"], enabled: !!account && !!bytecode });
const { data: cardActivity } = useQuery<CardActivity[]>({ queryKey: ["activity", "card"] });
const { data: cardLimitStatus } = useQuery<KYCStatus>({ queryKey: ["kyc", "cardLimit"], enabled: !!card });
const cardLimitProcessing = cardLimitStatus?.code === "processing";
const cardLimitApproved = cardLimitStatus?.code === "ok";
const spendingLimitReached = useMemo(() => {
if (!card?.limit.amount || !cardActivity) return false;
const limit = card.limit.amount / 100;
const totalSpent = cardActivity.reduce((sum, item) => {
if (item.type !== "panda") return sum;
const elapsed = (Date.now() - new Date(item.timestamp).getTime()) / 1000;
return elapsed <= 604_800 ? sum + item.usdAmount : sum;
}, 0);
return totalSpent / limit >= 0.9;
}, [card?.limit.amount, cardActivity]);
const { data: spotlightShown } = useQuery<boolean>({ queryKey: ["settings", "installments-spotlight"] });
const toast = useToastController();
const { mutate: mutateMode } = useMutation({
Expand Down Expand Up @@ -207,6 +223,32 @@ export default function Home() {
<View flex={1} gap="$s5" paddingBottom="$s5">
<YStack backgroundColor="$backgroundSoft" padding="$s4" gap="$s4">
{markets && healthFactor(markets) < HEALTH_FACTOR_THRESHOLD && <LiquidationAlert />}
{spendingLimitReached && !cardLimitProcessing && (
<InfoAlert
variant="warning"
title={t("You've reached 90% of your weekly card spending limit.")}
actionText={t("Increase spending limit")}
onPress={() => {
if (cardLimitApproved) newMessage(t("I want to increase my spending limit")).catch(reportError);
else
startCardLimitKYC()
.then((result) => {
if (result.status === "error")
toast.show(t("Something went wrong. Please try again."), {
native: true,
burntOptions: { haptic: "error", preset: "error" },
});
})
.catch((error: unknown) => {
reportError(error);
toast.show(t("Something went wrong. Please try again."), {
native: true,
burntOptions: { haptic: "error", preset: "error" },
});
});
Comment thread
franm91 marked this conversation as resolved.
}}
/>
)}
{(showKYCMigration || showPluginOutdated) && (
<InfoAlert
title={t(
Expand Down
52 changes: 35 additions & 17 deletions src/components/shared/InfoAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
import React from "react";
import { Pressable } from "react-native";

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

import Text from "./Text";

const variants = {
info: {
bg: "$interactiveBaseInformationSoftDefault",
iconBg: "$interactiveBaseInformationDefault",
icon: Info,
color: "$interactiveOnBaseInformationDefault",
text: "$interactiveOnBaseInformationSoft",
},
warning: {
bg: "$interactiveBaseWarningSoftDefault",
iconBg: "$interactiveBaseWarningDefault",
icon: AlertTriangle,
color: "$interactiveOnBaseWarningDefault",
text: "$interactiveOnBaseWarningSoft",
},
error: {
bg: "$interactiveBaseErrorSoftDefault",
iconBg: "$interactiveBaseErrorDefault",
icon: AlertTriangle,
color: "$interactiveOnBaseErrorDefault",
text: "$interactiveOnBaseErrorSoft",
},
} as const;

export default function InfoAlert({
title,
actionText,
loading,
onPress,
variant = "info",
}: {
actionText?: string;
loading?: boolean;
onPress?: () => void;
title: string;
variant?: keyof typeof variants;
}) {
const { bg, iconBg, icon: Icon, color, text } = variants[variant];
return (
<XStack borderRadius="$r3" backgroundColor="$interactiveBaseInformationSoftDefault" overflow="hidden">
<View
padding="$s4"
backgroundColor="$interactiveBaseInformationDefault"
justifyContent="center"
alignItems="center"
alignSelf="stretch"
>
<Info size={32} color="$interactiveOnBaseInformationDefault" />
<XStack borderRadius="$r3" backgroundColor={bg} overflow="hidden">
<View padding="$s4" backgroundColor={iconBg} justifyContent="center" alignItems="center" alignSelf="stretch">
<Icon size={32} color={color} />
</View>
<View gap="$s2" padding="$s4" flex={1}>
<Text subHeadline color="$interactiveOnBaseInformationSoft">
<Text subHeadline color={text}>
{title}
</Text>
<Pressable
Expand All @@ -39,14 +61,10 @@ export default function InfoAlert({
>
{actionText && (
<XStack gap="$s1" alignItems="center">
<Text emphasized subHeadline color="$interactiveOnBaseInformationSoft">
<Text emphasized subHeadline color={text}>
{actionText}
</Text>
{loading ? (
<Spinner color="$interactiveOnBaseInformationSoft" />
) : (
<ChevronRight size={16} color="$interactiveOnBaseInformationSoft" strokeWidth={3} />
)}
{loading ? <Spinner color={text} /> : <ChevronRight size={16} color={text} strokeWidth={3} />}
</XStack>
)}
</Pressable>
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/es-AR.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@
"You can repay early and save on interest. The final amount updates automatically before you confirm.": "Podés pagar antes y ahorrar en intereses. El monto final se actualiza automáticamente antes de confirmar.",
"You must repay each installment manually before its due date.": "Debés pagar cada cuota manualmente antes de su fecha de vencimiento.",
"You send": "Enviás",
"You've reached 90% of your weekly card spending limit.": "Alcanzaste el 90% de tu límite de gasto semanal.",
"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.",
"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.",
"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.",
"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.",
"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!",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,10 +656,12 @@
"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í.",
"You’re all set!": "¡Todo listo!",
"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.",
"You've reached 90% of your weekly card spending limit.": "Has alcanzado el 90% de tu límite de gasto semanal.",
"Your {{chain}} address": "Tu dirección en {{chain}}",
"Your address needs to be verified": "Tu dirección necesita ser verificada",
"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.",
"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.",
"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.",
"Your Exa account": "Tu cuenta Exa",
"Your Exa Card is now upgraded to Visa Signature.": "Tu Exa Card ha sido actualizada a Visa Signature.",
"Your funds serve as collateral to increase your spending limits.": "Tus fondos sirven como garantía para aumentar tus límites de gasto.",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,10 +656,12 @@
"You’re all caught up! Start using your card in Pay Later mode to see payments listed here.": "Você está em dia! Comece a usar seu cartão no modo Pagar Depois para ver os pagamentos aqui.",
"You’re all set!": "Tudo pronto!",
"You’re trying to borrow more than your collateral allows. Please enter a lower amount.": "Você está tentando emprestar mais do que sua garantia permite. Por favor, insira um valor menor.",
"You've reached 90% of your weekly card spending limit.": "Você atingiu 90% do seu limite de gastos semanal.",
"Your {{chain}} address": "Seu endereço na {{chain}}",
"Your address needs to be verified": "Seu endereço precisa ser verificado",
"Your card is awaiting activation. Follow the steps to enable it.": "Seu cartão está aguardando ativação. Siga os passos para ativá-lo.",
"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.",
"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.",
"Your Exa account": "Sua conta Exa",
"Your Exa Card is now upgraded to Visa Signature.": "Seu Exa Card foi atualizado para Visa Signature.",
"Your funds serve as collateral to increase your spending limits.": "Seus fundos servem como garantia para aumentar seus limites de gastos.",
Expand Down
Loading
Loading