Skip to content

Commit a20d207

Browse files
authored
Merge branch 'main' into yash/handle-token-by-index
2 parents aa541d8 + ddbacd1 commit a20d207

File tree

17 files changed

+269
-70
lines changed

17 files changed

+269
-70
lines changed

.changeset/wise-boxes-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
add `pastDue` to possible billing status flags

apps/dashboard/knip.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@
99
],
1010
"project": ["src/**"],
1111
"ignoreBinaries": ["only-allow", "biome"],
12-
"ignoreDependencies": ["@storybook/blocks", "@thirdweb-dev/service-utils"]
12+
"ignoreDependencies": [
13+
"@storybook/blocks",
14+
"@thirdweb-dev/service-utils",
15+
"@types/color"
16+
]
1317
}

apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
keepPreviousData,
3-
useMutation,
4-
useQuery,
5-
useQueryClient,
6-
} from "@tanstack/react-query";
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
72
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
83
import { useActiveAccount } from "thirdweb/react";
94
import type { WalletUser } from "thirdweb/wallets";
@@ -33,7 +28,6 @@ const fetchAccountList = ({
3328
const json = await res.json();
3429
return json as {
3530
users: WalletUser[];
36-
totalPages: number;
3731
};
3832
};
3933
};
@@ -57,11 +51,11 @@ export function useEmbeddedWallets(params: {
5751
clientId,
5852
pageNumber: page,
5953
}),
60-
placeholderData: keepPreviousData,
6154
enabled: !!address && !!clientId,
6255
});
6356
}
6457

58+
// TODO: fetching list of all users needs to be improved
6559
export function useAllEmbeddedWallets(params: {
6660
authToken: string;
6761
}) {
@@ -70,15 +64,13 @@ export function useAllEmbeddedWallets(params: {
7064
const address = useActiveAccount()?.address;
7165

7266
return useMutation({
73-
mutationFn: async ({
74-
clientId,
75-
totalPages,
76-
}: { clientId: string; totalPages: number }) => {
77-
const walletRes = [];
78-
for (let page = 1; page <= totalPages; page++) {
79-
const res = queryClient.fetchQuery<{
67+
mutationFn: async ({ clientId }: { clientId: string }) => {
68+
const responses: WalletUser[] = [];
69+
let page = 1;
70+
71+
while (true) {
72+
const res = await queryClient.fetchQuery<{
8073
users: WalletUser[];
81-
totalPages: number;
8274
}>({
8375
queryKey: embeddedWalletsKeys.embeddedWallets(
8476
address || "",
@@ -91,9 +83,16 @@ export function useAllEmbeddedWallets(params: {
9183
pageNumber: page,
9284
}),
9385
});
94-
walletRes.push(res);
86+
87+
if (res.users.length === 0) {
88+
break;
89+
}
90+
91+
page++;
92+
responses.push(...res.users);
9593
}
96-
return (await Promise.all(walletRes)).flatMap((res) => res.users);
94+
95+
return responses;
9796
},
9897
});
9998
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
PastDueBannerUI,
4+
ServiceCutOffBannerUI,
5+
} from "./BillingAlertBannersUI";
6+
7+
const meta = {
8+
title: "Banners/Billing Alerts",
9+
parameters: {
10+
layout: "centered",
11+
},
12+
} satisfies Meta;
13+
14+
export default meta;
15+
16+
type Story = StoryObj<typeof meta>;
17+
18+
export const PaymentAlerts: Story = {
19+
render: () => (
20+
<div className="space-y-10">
21+
<PastDueBannerUI
22+
teamSlug="foo"
23+
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
24+
/>
25+
26+
<ServiceCutOffBannerUI
27+
teamSlug="foo"
28+
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
29+
/>
30+
</div>
31+
),
32+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { redirectToBillingPortal } from "@/actions/billing";
4+
import {
5+
PastDueBannerUI,
6+
ServiceCutOffBannerUI,
7+
} from "./BillingAlertBannersUI";
8+
9+
export function PastDueBanner(props: { teamSlug: string }) {
10+
return (
11+
<PastDueBannerUI
12+
redirectToBillingPortal={redirectToBillingPortal}
13+
teamSlug={props.teamSlug}
14+
/>
15+
);
16+
}
17+
18+
export function ServiceCutOffBanner(props: { teamSlug: string }) {
19+
return (
20+
<ServiceCutOffBannerUI
21+
redirectToBillingPortal={redirectToBillingPortal}
22+
teamSlug={props.teamSlug}
23+
/>
24+
);
25+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import type { BillingBillingPortalAction } from "@/actions/billing";
4+
import { BillingPortalButton } from "@/components/billing";
5+
import { Spinner } from "@/components/ui/Spinner/Spinner";
6+
import { cn } from "@/lib/utils";
7+
import { useState } from "react";
8+
9+
function BillingAlertBanner(props: {
10+
title: string;
11+
description: React.ReactNode;
12+
teamSlug: string;
13+
variant: "error" | "warning";
14+
ctaLabel: string;
15+
redirectToBillingPortal: BillingBillingPortalAction;
16+
}) {
17+
const [isRouteLoading, setIsRouteLoading] = useState(false);
18+
19+
return (
20+
<div
21+
className={cn(
22+
"flex flex-col border-b bg-card px-4 py-6 lg:items-center lg:text-center",
23+
props.variant === "warning" &&
24+
"border-yellow-600 bg-yellow-50 text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-100",
25+
props.variant === "error" &&
26+
"border-red-600 bg-red-50 text-red-800 dark:border-red-700 dark:bg-red-950 dark:text-red-100",
27+
)}
28+
>
29+
<h3 className="font-semibold text-xl tracking-tight">{props.title}</h3>
30+
<p className="mt-1 mb-4 text-sm">{props.description}</p>
31+
<BillingPortalButton
32+
className={cn(
33+
"gap-2",
34+
props.variant === "warning" &&
35+
"border border-yellow-600 bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:border-yellow-700 dark:bg-yellow-900 dark:text-yellow-100 dark:hover:bg-yellow-800",
36+
props.variant === "error" &&
37+
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
38+
)}
39+
size="sm"
40+
teamSlug={props.teamSlug}
41+
redirectPath={`/team/${props.teamSlug}`}
42+
redirectToBillingPortal={props.redirectToBillingPortal}
43+
onClick={() => {
44+
setIsRouteLoading(true);
45+
}}
46+
>
47+
{props.ctaLabel}
48+
{isRouteLoading ? <Spinner className="size-4" /> : null}
49+
</BillingPortalButton>
50+
</div>
51+
);
52+
}
53+
54+
export function PastDueBannerUI(props: {
55+
teamSlug: string;
56+
redirectToBillingPortal: BillingBillingPortalAction;
57+
}) {
58+
return (
59+
<BillingAlertBanner
60+
ctaLabel="View Invoices"
61+
variant="warning"
62+
title="Unpaid Invoices"
63+
redirectToBillingPortal={props.redirectToBillingPortal}
64+
description={
65+
<>
66+
You have unpaid invoices. Service may be suspended if not paid
67+
promptly.
68+
</>
69+
}
70+
teamSlug={props.teamSlug}
71+
/>
72+
);
73+
}
74+
75+
export function ServiceCutOffBannerUI(props: {
76+
teamSlug: string;
77+
redirectToBillingPortal: BillingBillingPortalAction;
78+
}) {
79+
return (
80+
<BillingAlertBanner
81+
ctaLabel="Pay Now"
82+
variant="error"
83+
title="Service Suspended"
84+
redirectToBillingPortal={props.redirectToBillingPortal}
85+
description={
86+
<>
87+
Your service has been suspended due to unpaid invoices. Pay now to
88+
resume service.
89+
</>
90+
}
91+
teamSlug={props.teamSlug}
92+
/>
93+
);
94+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export default async function Page(props: {
3030
);
3131

3232
const hasSmartWalletsWithoutBilling =
33-
isBundlerServiceEnabled && team.billingStatus !== "validPayment";
33+
isBundlerServiceEnabled &&
34+
team.billingStatus !== "validPayment" &&
35+
team.billingStatus !== "pastDue";
3436

3537
const userOpStats = await getAggregateUserOpUsage({
3638
teamId: team.id,

apps/dashboard/src/app/team/[team_slug]/layout.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { AppFooter } from "@/components/blocks/app-footer";
33
import { redirect } from "next/navigation";
44
import { TWAutoConnect } from "../../components/autoconnect";
55
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
6+
import {
7+
PastDueBanner,
8+
ServiceCutOffBanner,
9+
} from "./(team)/_components/BillingAlertBanners";
610

711
export default async function RootTeamLayout(props: {
812
children: React.ReactNode;
@@ -17,8 +21,19 @@ export default async function RootTeamLayout(props: {
1721

1822
return (
1923
<div className="flex min-h-dvh flex-col">
20-
<div className="flex grow flex-col">{props.children}</div>
24+
<div className="flex grow flex-col">
25+
{team.billingStatus === "pastDue" && (
26+
<PastDueBanner teamSlug={team_slug} />
27+
)}
28+
29+
{team.billingStatus === "invalidPayment" && (
30+
<ServiceCutOffBanner teamSlug={team_slug} />
31+
)}
32+
33+
{props.children}
34+
</div>
2135
<TWAutoConnect />
36+
2237
<AppFooter />
2338
<SaveLastVisitedTeamPage />
2439
</div>

apps/dashboard/src/app/team/components/TeamHeader/getValidTeamPlan.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { Team } from "@/api/team";
22

33
export function getValidTeamPlan(team: Team): Team["billingPlan"] {
4-
if (team.billingStatus !== "validPayment") {
4+
if (
5+
team.billingStatus !== "validPayment" &&
6+
team.billingStatus !== "pastDue"
7+
) {
58
return "free";
69
}
710

0 commit comments

Comments
 (0)