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: 5 additions & 0 deletions .changeset/wet-needles-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

✨ add card limit kyc flow
11 changes: 3 additions & 8 deletions src/components/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import useAsset from "../../utils/useAsset";
import useBeginKYC from "../../utils/useBeginKYC";
import useMarkets from "../../utils/useMarkets";
import useTabPress from "../../utils/useTabPress";
import weeklySpend from "../../utils/weeklySpend";
import FundingAlert from "../shared/FundingAlert";
import IconButton from "../shared/IconButton";
import InfoAlert from "../shared/InfoAlert";
Expand Down Expand Up @@ -83,14 +84,7 @@ export default function Card() {
} = useQuery<CardDetailsData>({ queryKey: ["card", "details"], retry: false, gcTime: 0, staleTime: 0 });

const limit = cardDetails?.limit.amount ? cardDetails.limit.amount / 100 : undefined;
const weeklyPurchases = purchases
? purchases.filter((item): item is Extract<CardActivity, { type: "panda" }> => {
if (item.type !== "panda" || item.status === "declined") return false;
const elapsedTime = (Date.now() - new Date(item.timestamp).getTime()) / 1000;
return elapsedTime <= 604_800;
})
: [];
const totalSpent = weeklyPurchases.reduce((accumulator, item) => accumulator + item.usdAmount, 0);
const totalSpent = weeklySpend(purchases);

const { queryKey } = useAsset(marketUSDCAddress);
const { address } = useAccount();
Expand Down Expand Up @@ -122,6 +116,7 @@ export default function Card() {
Promise.all([
refetchCard(),
queryClient.invalidateQueries({ queryKey: ["activity", "card"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["kyc", "cardLimit"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["kyc", "status"], exact: true }),
address ? refetchBytecode() : undefined,
address ? refetchMarkets() : undefined,
Expand Down
38 changes: 25 additions & 13 deletions src/components/card/SpendingLimits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Plus } from "@tamagui/lucide-icons";
import { ScrollView, XStack, YStack } from "tamagui";

import SpendingLimit from "./SpendingLimit";
import { newMessage } from "../../utils/intercom";
import reportError from "../../utils/reportError";
import useCardLimit from "../../utils/useCardLimit";
import InfoAlert from "../shared/InfoAlert";
import ModalSheet from "../shared/ModalSheet";
import SafeView from "../shared/SafeView";
import Button from "../shared/StyledButton";
Expand All @@ -26,6 +26,7 @@ export default function SpendingLimits({
totalSpent: number;
}) {
const { t } = useTranslation();
const { increase, pending, processing } = useCardLimit(open && limit != null);
Comment thread
dieguezguille marked this conversation as resolved.
return (
<ModalSheet open={open} onClose={onClose}>
<SafeView paddingTop={0} fullScreen borderTopLeftRadius="$r4" borderTopRightRadius="$r4">
Expand All @@ -44,17 +45,28 @@ 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();
increase();
}}
primary
disabled={pending}
loading={pending}
>
<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
24 changes: 22 additions & 2 deletions src/components/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
import { cardModeMutationOptions } from "../../utils/server";
import useAccount from "../../utils/useAccount";
import useCardLimit from "../../utils/useCardLimit";
import useMarkets from "../../utils/useMarkets";
import usePendingOperations from "../../utils/usePendingOperations";
import usePortfolio from "../../utils/usePortfolio";
import useTabPress from "../../utils/useTabPress";
import weeklySpend from "../../utils/weeklySpend";
import BenefitsSection from "../benefits/BenefitsSection";
import CardDetailsSheet from "../card/CardDetails";
import ManualRepaymentSheet from "../pay/ManualRepaymentSheet";
Expand All @@ -61,7 +63,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 @@ -138,6 +140,13 @@ 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"] });
Comment thread
dieguezguille marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid duplicating the Home activity fetch

When Home mounts, this new ['activity', 'card'] query issues a second /api/activity?include=card request even though the existing ['activity'] query on the same screen already includes card items when include is omitted; the server's activity handler only filters types when include is set, and the card branch scans card borrow logs from block 0. This doubles the expensive activity work on every Home load just to compute the 90% alert, so derive the weekly spend from the already-loaded activity data or only enable the card-only query when the full activity query is unavailable.

Useful? React with 👍 / 👎.

const spendingLimitReached = !!card?.limit.amount && weeklySpend(cardActivity) / (card.limit.amount / 100) >= 0.9;
const {
increase: increaseLimit,
pending: cardLimitPending,
processing: cardLimitProcessing,
} = useCardLimit(spendingLimitReached);
const { data: spotlightShown } = useQuery<boolean>({ queryKey: ["settings", "installments-spotlight"] });
const { data: lastInstallments } = useQuery<number>({ queryKey: ["settings", "installments"] });
const { data: promoSeen } = useQuery<boolean>({ queryKey: ["settings", "promo-seen", PROMO.id] });
Expand Down Expand Up @@ -212,6 +221,9 @@ export default function Home() {
const refresh = () =>
Promise.all([
queryClient.invalidateQueries({ queryKey: ["activity"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["activity", "card"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["card", "details"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["kyc", "cardLimit"], exact: true }),
queryClient.invalidateQueries({ queryKey: ["kyc", "status"], exact: true }),
revalidateUnsupported(),
account ? refetchMarkets() : undefined,
Expand Down Expand Up @@ -245,7 +257,7 @@ export default function Home() {
<YStack backgroundColor="$backgroundSoft" padding="$s4" gap="$s4">
{overdueMaturity !== undefined && (
<InfoAlert
error
variant="error"
title={t("You have an overdue payment. Pay now to avoid additional interest.")}
actionText={t("Pay now")}
onPress={() => {
Expand All @@ -254,6 +266,14 @@ export default function Home() {
/>
)}
{markets && healthFactor(markets) < HEALTH_FACTOR_THRESHOLD && <LiquidationAlert />}
{spendingLimitReached && !cardLimitPending && !cardLimitProcessing && (
<InfoAlert
variant="warning"
title={t("You've reached 90% of your weekly card spending limit.")}
actionText={t("Increase spending limit")}
onPress={increaseLimit}
/>
)}
{(showKYCMigration || showPluginOutdated) && (
<InfoAlert
title={t(
Expand Down
57 changes: 34 additions & 23 deletions src/components/shared/InfoAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,28 @@ import { AlertTriangle, ChevronRight, Info } from "@tamagui/lucide-icons";
import { Spinner, View, XStack } from "tamagui";

import Text from "./Text";

export default function InfoAlert({
title,
actionText,
error,
loading,
onPress,
variant = "info",
}: {
actionText?: string;
error?: boolean;
loading?: boolean;
onPress?: () => void;
title: string;
variant?: keyof typeof variants;
}) {
const onBase = error ? "$interactiveOnBaseErrorSoft" : "$interactiveOnBaseInformationSoft";
const { bg, iconBg, icon: Icon, color, text } = variants[variant];
return (
<XStack
borderRadius="$r3"
backgroundColor={error ? "$interactiveBaseErrorSoftDefault" : "$interactiveBaseInformationSoftDefault"}
overflow="hidden"
>
<View
padding="$s4"
backgroundColor={error ? "$interactiveBaseErrorDefault" : "$interactiveBaseInformationDefault"}
justifyContent="center"
alignItems="center"
alignSelf="stretch"
>
{error ? (
<AlertTriangle size={32} color="$interactiveOnBaseErrorDefault" />
) : (
<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={onBase}>
<Text subHeadline color={text}>
{title}
</Text>
<Pressable
Expand All @@ -50,14 +37,38 @@ export default function InfoAlert({
>
{actionText && (
<XStack gap="$s1" alignItems="center">
<Text emphasized subHeadline color={onBase}>
<Text emphasized subHeadline color={text}>
{actionText}
</Text>
{loading ? <Spinner color={onBase} /> : <ChevronRight size={16} color={onBase} strokeWidth={3} />}
{loading ? <Spinner color={text} /> : <ChevronRight size={16} color={text} strokeWidth={3} />}
</XStack>
)}
</Pressable>
</View>
</XStack>
);
}

const variants = {
error: {
bg: "$interactiveBaseErrorSoftDefault",
iconBg: "$interactiveBaseErrorDefault",
icon: AlertTriangle,
color: "$interactiveOnBaseErrorDefault",
text: "$interactiveOnBaseErrorSoft",
},
info: {
bg: "$interactiveBaseInformationSoftDefault",
iconBg: "$interactiveBaseInformationDefault",
icon: Info,
color: "$interactiveOnBaseInformationDefault",
text: "$interactiveOnBaseInformationSoft",
},
warning: {
bg: "$interactiveBaseWarningSoftDefault",
iconBg: "$interactiveBaseWarningDefault",
icon: AlertTriangle,
color: "$interactiveOnBaseWarningDefault",
text: "$interactiveOnBaseWarningSoft",
},
} as const;
2 changes: 2 additions & 0 deletions src/i18n/es-AR.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,10 @@
"You have an overdue payment. Pay now to avoid additional interest.": "Tenés un pago vencido. Pagá ahora para evitar intereses adicionales.",
"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 assets can't back your card yet. Swap them to a supported asset to start spending.": "Tus activos aún no pueden respaldar tu tarjeta. Intercambialos por un activo compatible para empezar a gastar.",
"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 @@ -821,6 +821,7 @@
"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 assets can't back your card yet. Swap them to a supported asset to start spending.": "Tus activos aún no pueden respaldar tu tarjeta. Intercámbialos por un activo compatible para empezar a gastar.",
Expand All @@ -834,6 +835,7 @@
"Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Tus fondos sirven como garantía, aumentando tus límites de gasto. Cuantos más fondos agregues, más podrás gastar con la Exa Card.",
"Your ID needs to be updated": "Tu documento necesita ser actualizado",
"Your KYC isn't approved for this currency": "Tu KYC no está aprobado para esta moneda",
"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 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, prueba con otro.",
"Your portfolio": "Tu cartera",
"Your Portfolio": "Tu cartera",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@
"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 assets can't back your card yet. Swap them to a supported asset to start spending.": "Seus ativos ainda não podem servir de garantia para o seu cartão. Troque-os por um ativo compatível para começar a gastar.",
Expand All @@ -834,6 +835,7 @@
"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.",
"Your ID needs to be updated": "Seu documento precisa ser atualizado",
"Your KYC isn't approved for this currency": "Seu KYC não está aprovado para esta moeda",
"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 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",
"Your portfolio": "Seu portfólio",
"Your Portfolio": "Seu portfólio",
Expand Down
Loading
Loading