Skip to content

Commit f31af02

Browse files
authored
Merge pull request #409 from Merit-Systems/br/free-tier-conversion
[Feature] Update your Free Tier with your Echo Balance
2 parents 20f98e6 + 8eb5e84 commit f31af02

File tree

10 files changed

+295
-28
lines changed

10 files changed

+295
-28
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterEnum
2+
ALTER TYPE "public"."EnumPaymentSource" ADD VALUE 'balance';

packages/app/control/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ enum EnumPaymentSource {
283283
stripe
284284
admin
285285
signUpGift
286+
balance
286287
}
287288

288289
model Transaction {

packages/app/control/src/app/(app)/(home)/credits/_components/balance/add-credits.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { useState } from 'react';
55
import { Button } from '@/components/ui/button';
66
import { MoneyInput } from '@/components/ui/money-input';
77

8-
import { Check, Loader2 } from 'lucide-react';
8+
import { AlertCircle, Check, Loader2 } from 'lucide-react';
99
import { api } from '@/trpc/client';
1010

1111
export const AddCredits = () => {
1212
const [amount, setAmount] = useState<number>();
13+
const [error, setError] = useState<string | null>(null);
1314

1415
const {
1516
mutate: createPaymentLink,
@@ -19,6 +20,9 @@ export const AddCredits = () => {
1920
onSuccess: data => {
2021
window.location.href = data.paymentLink.url;
2122
},
23+
onError: error => {
24+
setError(error.message || 'Failed to create payment link');
25+
},
2226
});
2327

2428
const onAddCredits = () => {
@@ -37,6 +41,12 @@ export const AddCredits = () => {
3741
min="1"
3842
step="0.01"
3943
/>
44+
{error && (
45+
<div className="flex items-center gap-2 p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
46+
<AlertCircle className="size-4 flex-shrink-0" />
47+
<span>{error}</span>
48+
</div>
49+
)}
4050
<Button
4151
onClick={onAddCredits}
4252
disabled={isPending || !amount || amount <= 0 || isSuccess}

packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ export const HeaderCard: React.FC<Props> = ({ appId }) => {
3535
<div className="grid grid-cols-1 md:grid-cols-7">
3636
<div className="flex flex-col gap-4 p-4 pt-12 md:pt-14 col-span-5">
3737
<div className="">
38-
<h1 className="text-3xl font-bold">{app.name}</h1>
38+
<h1 className="text-3xl font-bold break-words line-clamp-2">
39+
{app.name}
40+
</h1>
3941
<p
40-
className={
42+
className={cn(
43+
'break-words line-clamp-2',
4144
app.description
4245
? 'text-muted-foreground'
4346
: 'text-muted-foreground/40'
44-
}
47+
)}
4548
>
4649
{app.description ?? 'No description'}
4750
</p>

packages/app/control/src/app/(app)/app/[id]/free-tier/_components/balance/add-credits.tsx

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useState } from 'react';
55
import { Button } from '@/components/ui/button';
66
import { MoneyInput } from '@/components/ui/money-input';
77

8-
import { Check, Loader2 } from 'lucide-react';
8+
import { Check, Loader2, AlertCircle } from 'lucide-react';
99
import { api } from '@/trpc/client';
1010

1111
interface Props {
@@ -14,24 +14,69 @@ interface Props {
1414

1515
export const AddCredits: React.FC<Props> = ({ appId }) => {
1616
const [amount, setAmount] = useState<number>();
17+
const [error, setError] = useState<string | null>(null);
18+
const [isSuccessFromBalance, setIsSuccessFromBalance] =
19+
useState<boolean>(false);
20+
21+
const utils = api.useUtils();
1722

1823
const {
1924
mutate: createPaymentLink,
2025
isPending,
2126
isSuccess,
2227
} = api.apps.app.freeTier.payments.create.useMutation({
2328
onSuccess: data => {
29+
setError(null);
2430
window.location.href = data.paymentLink.url;
2531
},
32+
onError: error => {
33+
setError(error.message || 'Failed to create payment link');
34+
},
35+
});
36+
37+
const {
38+
mutate: createPaymentFromBalance,
39+
isPending: isPendingFromBalance,
40+
error: balancePaymentError,
41+
} = api.apps.app.freeTier.payments.createFromBalance.useMutation({
42+
onSuccess: data => {
43+
if (!data.success) {
44+
setError(data.error_message || 'Payment failed');
45+
return;
46+
}
47+
setError(null);
48+
setIsSuccessFromBalance(true);
49+
50+
// Invalidate balance queries
51+
utils.user.balance.get.invalidate();
52+
utils.user.balance.app.free.invalidate({ appId });
53+
utils.apps.app.freeTier.get.invalidate({ appId });
54+
},
55+
onError: error => {
56+
setError(error.message || 'Failed to process payment from balance');
57+
},
2658
});
2759

2860
const onAddCredits = () => {
29-
if (!amount) {
30-
throw new Error('Amount is required');
61+
if (!amount || amount <= 0) {
62+
setError('Please enter a valid amount');
63+
return;
3164
}
65+
setError(null);
3266
createPaymentLink({ appId, amount });
3367
};
3468

69+
const onAddCreditsFromBalance = () => {
70+
if (!amount || amount <= 0) {
71+
setError('Please enter a valid amount');
72+
return;
73+
}
74+
setError(null);
75+
createPaymentFromBalance({ appId, amountInDollars: amount });
76+
};
77+
78+
const [currentUserBalance] = api.user.balance.get.useSuspenseQuery();
79+
3580
return (
3681
<div className="flex flex-col w-full gap-4">
3782
<MoneyInput
@@ -41,20 +86,52 @@ export const AddCredits: React.FC<Props> = ({ appId }) => {
4186
min="1"
4287
step="0.01"
4388
/>
44-
<Button
45-
onClick={onAddCredits}
46-
disabled={isPending || !amount || amount <= 0 || isSuccess}
47-
size="lg"
48-
variant="turbo"
49-
>
50-
{isPending ? (
51-
<Loader2 className="size-4 animate-spin" />
52-
) : isSuccess ? (
53-
<Check className="size-4" />
54-
) : (
55-
'Add Credits'
56-
)}
57-
</Button>
89+
90+
{error && (
91+
<div className="flex items-center gap-2 p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
92+
<AlertCircle className="size-4 flex-shrink-0" />
93+
<span>{error}</span>
94+
</div>
95+
)}
96+
97+
<div className="flex gap-3">
98+
<Button
99+
onClick={onAddCredits}
100+
disabled={isPending || !amount || amount < 1 || isSuccess}
101+
size="lg"
102+
variant="turboSecondary"
103+
className="flex-1"
104+
>
105+
{isPending ? (
106+
<Loader2 className="size-4 animate-spin" />
107+
) : isSuccess ? (
108+
<Check className="size-4" />
109+
) : (
110+
'Add Credits'
111+
)}
112+
</Button>
113+
<Button
114+
onClick={onAddCreditsFromBalance}
115+
disabled={
116+
isPendingFromBalance ||
117+
!amount ||
118+
amount < 0 ||
119+
isSuccessFromBalance ||
120+
currentUserBalance.balance < amount
121+
}
122+
size="lg"
123+
variant="turbo"
124+
className="flex-1"
125+
>
126+
{isPendingFromBalance ? (
127+
<Loader2 className="size-4 animate-spin" />
128+
) : isSuccessFromBalance && !balancePaymentError ? (
129+
<Check className="size-4" />
130+
) : (
131+
'Add Credits From Balance'
132+
)}
133+
</Button>
134+
</div>
58135
</div>
59136
);
60137
};

packages/app/control/src/app/(app)/app/[id]/free-tier/_components/balance/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import {
1313
DialogTitle,
1414
DialogDescription,
1515
DialogTrigger,
16+
DialogFooter,
1617
} from '@/components/ui/dialog';
1718
import { Skeleton } from '@/components/ui/skeleton';
19+
import { Balance as BalanceDisplay } from '@/app/(app)/_components/layout/header/balance/balance-display';
1820

1921
import { AddCredits } from './add-credits';
2022

@@ -29,7 +31,9 @@ interface Props {
2931
const BalanceContainer = ({ children }: { children: React.ReactNode }) => {
3032
return (
3133
<Card className="border rounded-lg overflow-hidden flex flex-col gap-2 p-4">
32-
<h1 className="text-lg font-semibold text-muted-foreground">Balance</h1>
34+
<h1 className="text-lg font-semibold text-muted-foreground">
35+
Free Tier Balance
36+
</h1>
3337
<div className="flex items-center gap-4 w-full">{children}</div>
3438
</Card>
3539
);
@@ -58,6 +62,12 @@ export const Balance: React.FC<Props> = ({ appId }) => {
5862
Your users will be able to use free tier credits before they have
5963
to buy credits and spend their Echo balance.
6064
</DialogDescription>
65+
<DialogFooter>
66+
<span className="text-sm font-semibold text-muted-foreground">
67+
Current Balance:
68+
</span>
69+
<BalanceDisplay />
70+
</DialogFooter>
6171
</DialogHeader>
6272
<AddCredits appId={appId} />
6373
</DialogContent>

packages/app/control/src/components/ui/button.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ const buttonVariants = cva(
3232
'before:content-[""] before:absolute before:w-full before:h-full before:rounded-md before:pointer-events-none',
3333
'before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:animate-shimmer'
3434
),
35+
turboSecondary: cn(
36+
'bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600 text-white hover:opacity-90',
37+
'shadow-[0_2px_6px_color-mix(in_oklab,theme(colors.gray.500)_70%,transparent)]',
38+
'hover:shadow-[0_2px_4px_color-mix(in_oklab,theme(colors.gray.500)_70%,transparent)]',
39+
'inset-ring-2 inset-ring-inset inset-ring-border/50',
40+
'relative overflow-hidden',
41+
'before:content-[""] before:absolute before:w-full before:h-full before:rounded-md before:pointer-events-none',
42+
'before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:animate-shimmer'
43+
),
3544
unstyled: '',
3645
},
3746
size: {

0 commit comments

Comments
 (0)