New USDC available redesign, NEXT_PUBLIC_ZKP2P_IFRAME_HOST env. variable

This commit is contained in:
Luis Rojas
2025-08-26 22:56:45 -04:00
parent 44b10d16a5
commit 3937fcc669
7 changed files with 137 additions and 62 deletions
+3
View File
@@ -157,3 +157,6 @@ NOW_PAYMENTS_IPN_KEY=key
COINBASE_API_URL=http://localhost
COINBASE_API_KEY=key
COINBASE_WEBHOOK_SECRET=secret
ZKP2P_BASE_URL=https://zkp2p.xyz
NEXT_PUBLIC_ZKP2P_IFRAME_HOST=http://localhost:3001
@@ -29,7 +29,10 @@ import { useBuzzTransactions, useTransactionsReport } from '~/components/Buzz/us
import { DaysFromNow } from '~/components/Dates/DaysFromNow';
import { UserBuzz } from '~/components/User/UserBuzz';
import { BuzzTopUpCard } from '~/components/Buzz/BuzzTopUpCard';
import { USDCPurchasePrompt } from '~/components/Buzz/USDCPurchasePrompt';
import {
USDCPurchasePrompt,
useUSDCPurchasePromptVisibility,
} from '~/components/Buzz/USDCPurchasePrompt';
import type { GetTransactionsReportSchema } from '~/server/schema/buzz.schema';
import { TransactionType } from '~/server/schema/buzz.schema';
import { formatDate } from '~/utils/date-helpers';
@@ -56,6 +59,7 @@ const INCLUDE_DESCRIPTION = [TransactionType.Reward, TransactionType.Purchase];
export const BuzzDashboardOverview = ({ accountId }: { accountId: number }) => {
const theme = useMantineTheme();
const features = useFeatureFlags();
const { shouldShow: shouldShowUSDCPrompt } = useUSDCPurchasePromptVisibility(accountId);
// Right now, sadly, we neeed to use two separate queries for user and generation transactions.
// If this ever changes, that'd be awesome. But for now, we need to do this.
const [transactionType, setTransactionType] = React.useState<'user' | 'generation'>('user');
@@ -228,14 +232,16 @@ export const BuzzDashboardOverview = ({ accountId }: { accountId: number }) => {
{/* USDC Purchase Prompt - Show when user has USDC available */}
<USDCPurchasePrompt userId={accountId} />
{/* Top Up Card - Show when buzz is low */}
<BuzzTopUpCard
accountId={accountId}
variant="banner"
message="Need more Buzz?"
showBalance={false}
btnLabel="Top up"
/>
{/* Top Up Card - Show when buzz is low and no USDC prompt */}
{!shouldShowUSDCPrompt && (
<BuzzTopUpCard
accountId={accountId}
variant="banner"
message="Need more Buzz?"
showBalance={false}
btnLabel="Top up"
/>
)}
<SegmentedControl
value={reportFilters.window}
@@ -0,0 +1,46 @@
.usdcCard {
background: light-dark(
var(--mantine-color-teal-0),
var(--mantine-color-dark-6)
);
border: 1px solid light-dark(var(--mantine-color-teal-2), var(--mantine-color-teal-9));
border-left: 4px solid var(--mantine-color-teal-5);
transition: all 0.2s ease;
&:hover {
box-shadow: var(--mantine-shadow-sm);
}
}
.loadingCard {
background: light-dark(
var(--mantine-color-teal-0),
var(--mantine-color-dark-6)
);
border: 1px solid light-dark(var(--mantine-color-teal-2), var(--mantine-color-teal-9));
border-left: 4px solid var(--mantine-color-teal-5);
transition: all 0.2s ease;
&:hover {
box-shadow: var(--mantine-shadow-sm);
}
}
.usdcIcon {
box-shadow: var(--mantine-shadow-sm);
}
.usdcTitle {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-gray-0));
}
/* Responsive adjustments */
@media (max-width: 768px) {
.usdcCard {
padding: var(--mantine-spacing-sm);
}
.loadingCard {
padding: var(--mantine-spacing-sm);
}
}
+68 -51
View File
@@ -1,21 +1,21 @@
import { Button, Group, Text, Loader } from '@mantine/core';
import { IconBolt } from '@tabler/icons-react';
import { Button, Card, Group, Stack, Text, Loader, ThemeIcon, Badge, Center } from '@mantine/core';
import { IconBolt, IconWallet, IconArrowRight, IconCurrencyDollar } from '@tabler/icons-react';
import { useState, useEffect } from 'react';
import { useCoinbaseOnrampBalance, useMutateCoinbase } from '~/components/Coinbase/util';
import { usdcToBuzz } from '~/utils/buzz';
import { numberWithCommas } from '~/utils/number-helpers';
import { trpc } from '~/utils/trpc';
import { CryptoTransactionStatus } from '~/shared/utils/prisma/enums';
import classes from './USDCPurchasePrompt.module.scss';
interface USDCPurchasePromptProps {
userId?: number;
}
export const USDCPurchasePrompt = ({ userId }: USDCPurchasePromptProps) => {
const currentUserId = userId;
// Hook to check if USDC prompt should show (for other components to use)
export const useUSDCPurchasePromptVisibility = (userId?: number) => {
const [shouldShow, setShouldShow] = useState(false);
// First check if user has pending transactions in database
const { data: transactions, isLoading: isLoadingTransactions } =
trpc.coinbase.getPaginatedUserTransactions.useQuery(
{
@@ -23,19 +23,14 @@ export const USDCPurchasePrompt = ({ userId }: USDCPurchasePromptProps) => {
page: 1,
},
{
enabled: !!currentUserId,
enabled: !!userId,
}
);
// Only check wallet balance if we found pending transactions
const { data: balanceData, isLoading: isLoadingBalance } = useCoinbaseOnrampBalance();
const { processUserPendingTransactions, processingUserPendingTransactions } = useMutateCoinbase();
const balance = balanceData?.balance ?? 0;
const buzzAmount = usdcToBuzz(balance);
useEffect(() => {
// Check if user has any crypto transactions that indicate they might have USDC
if (transactions?.items?.length) {
const hasPendingOrSuccess = transactions.items.some(
(t) =>
@@ -49,6 +44,20 @@ export const USDCPurchasePrompt = ({ userId }: USDCPurchasePromptProps) => {
}
}, [transactions, balance]);
return {
shouldShow: shouldShow && !isLoadingTransactions,
isLoading: isLoadingTransactions || isLoadingBalance,
};
};
export const USDCPurchasePrompt = ({ userId }: USDCPurchasePromptProps) => {
const { shouldShow, isLoading } = useUSDCPurchasePromptVisibility(userId);
const { data: balanceData, isLoading: isLoadingBalance } = useCoinbaseOnrampBalance();
const { processUserPendingTransactions, processingUserPendingTransactions } = useMutateCoinbase();
const balance = balanceData?.balance ?? 0;
const buzzAmount = usdcToBuzz(balance);
const handlePurchase = async () => {
if (processingUserPendingTransactions || isLoadingBalance || balance <= 0) {
return;
@@ -62,54 +71,62 @@ export const USDCPurchasePrompt = ({ userId }: USDCPurchasePromptProps) => {
};
// Don't render anything during initial load or if conditions aren't met
if (!shouldShow || isLoadingTransactions) {
if (!shouldShow || isLoading) {
return null;
}
if (isLoadingBalance) {
return (
<Group
gap="sm"
p="md"
style={{
backgroundColor: 'var(--mantine-color-teal-1)',
borderRadius: 'var(--mantine-radius-md)',
border: '2px solid var(--mantine-color-teal-4)',
}}
>
<Loader size="sm" />
<Text size="sm" c="teal.8">
Checking USDC balance...
</Text>
</Group>
<Card className={classes.loadingCard} padding="md" radius="md" withBorder>
<Group gap="sm" justify="center">
<Loader size="sm" color="teal" />
<Text size="sm" c="teal.8" fw={500}>
Checking USDC balance...
</Text>
</Group>
</Card>
);
}
return (
<Group
justify="space-between"
wrap="wrap"
p="md"
gap="md"
style={{
backgroundColor: 'var(--mantine-color-teal-1)',
borderRadius: 'var(--mantine-radius-md)',
border: '2px solid var(--mantine-color-teal-4)',
}}
<Card
className={classes.usdcCard}
padding="md"
radius="md"
withBorder
>
<Text size="sm" fw={600} c="teal.8">
💰 You have ${balance.toFixed(2)} USDC available
</Text>
<Button
loading={processingUserPendingTransactions}
onClick={handlePurchase}
color="teal"
size="sm"
fw={500}
leftSection={<IconBolt size={16} fill="currentColor" />}
>
Purchase {numberWithCommas(buzzAmount)} Buzz
</Button>
</Group>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<ThemeIcon
size="lg"
variant="gradient"
gradient={{ from: 'teal.4', to: 'green.5' }}
radius="md"
className={classes.usdcIcon}
>
<IconWallet size={24} />
</ThemeIcon>
<div style={{ flex: 1 }}>
<Text size="sm" fw={600} className={classes.usdcTitle}>
${balance.toFixed(2)} USDC Available
</Text>
<Text size="xs" c="dimmed">
Purchase {numberWithCommas(buzzAmount)} Buzz instantly
</Text>
</div>
</Group>
<Button
loading={processingUserPendingTransactions}
onClick={handlePurchase}
variant="gradient"
gradient={{ from: 'teal.4', to: 'green.5' }}
size="sm"
leftSection={<IconBolt size={16} fill="currentColor" />}
radius="md"
>
Purchase Buzz
</Button>
</Group>
</Card>
);
};
+2
View File
@@ -38,6 +38,7 @@ export const clientSchema = z.object({
NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITEKEY: z.string().optional(),
NEXT_PUBLIC_CF_INVISIBLE_TURNSTILE_SITEKEY: z.string().optional(),
NEXT_PUBLIC_CF_MANAGED_TURNSTILE_SITEKEY: z.string().optional(),
NEXT_PUBLIC_ZKP2P_IFRAME_HOST: z.string().optional(),
});
/**
@@ -80,4 +81,5 @@ export const clientEnv = {
NEXT_PUBLIC_CF_INVISIBLE_TURNSTILE_SITEKEY:
process.env.NEXT_PUBLIC_CF_INVISIBLE_TURNSTILE_SITEKEY,
NEXT_PUBLIC_CF_MANAGED_TURNSTILE_SITEKEY: process.env.NEXT_PUBLIC_CF_MANAGED_TURNSTILE_SITEKEY,
NEXT_PUBLIC_ZKP2P_IFRAME_HOST: process.env.NEXT_PUBLIC_ZKP2P_IFRAME_HOST,
};
+1
View File
@@ -246,4 +246,5 @@ export const serverSchema = z.object({
// ZKP2P Related:
ZKP2P_BASE_URL: z.string().optional(),
ZKP2P_IFRAME_HOST: z.string().optional(),
});
+2 -2
View File
@@ -6,9 +6,9 @@ import { createServerSideProps } from '~/server/utils/server-side-helpers';
import { Meta } from '~/components/Meta/Meta';
import { trackZkp2pEvent, generateZkp2pSessionId } from '~/utils/zkp2p-tracking';
import Script from 'next/script';
import { env } from '~/env/client';
// const ZKP2P_IFRAME_HOST = 'https://zkp2p.civitai.com';
const ZKP2P_IFRAME_HOST = 'http://localhost:3001';
const ZKP2P_IFRAME_HOST = env.NEXT_PUBLIC_ZKP2P_IFRAME_HOST || 'http://localhost:3001';
export default function Zkp2pPurchasePage() {
const router = useRouter();