account payments tab fix (#1147)

This commit is contained in:
BilalG1 2026-02-02 15:22:19 -08:00 committed by GitHub
parent 0f8b23dda1
commit 0f6ae49cd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 97 additions and 12 deletions

View File

@ -1,8 +1,9 @@
'use client';
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { Skeleton, Typography } from '@stackframe/stack-ui';
import { Contact, ShieldCheck, Bell, Monitor, Key, Settings, CirclePlus, CreditCard } from 'lucide-react';
import React, { Suspense } from "react";
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useStackApp, useUser } from '..';
import { MaybeFullPage } from "../components/elements/maybe-full-page";
import { SidebarLayout } from '../components/elements/sidebar-layout';
@ -88,6 +89,69 @@ export function AccountSettings(props: {
const project = props.mockProject || projectFromHook;
const teams = user?.useTeams() || [];
const billing = user?.useBilling() || null;
const teamsKey = useMemo(() => teams.map(team => team.id).join("|"), [teams]);
const teamsById = useMemo(() => teams, [teamsKey]);
const userRef = useRef(userFromHook ?? null);
const userId = userFromHook?.id ?? null;
const [paymentsAvailability, setPaymentsAvailability] = useState<{
userHasProducts: boolean,
teamIdsWithProducts: Set<string>,
isReady: boolean,
}>(() => ({
userHasProducts: false,
teamIdsWithProducts: new Set<string>(),
isReady: !!props.mockUser,
}));
useEffect(() => {
userRef.current = userFromHook ?? null;
}, [userFromHook]);
useEffect(() => {
if (props.mockUser || !userId) {
return;
}
let cancelled = false;
runAsynchronouslyWithAlert(async () => {
const currentUser = userRef.current;
if (!currentUser || currentUser.id !== userId) {
return;
}
const [userProducts, teamsWithProducts] = await Promise.all([
currentUser.listProducts({ limit: 1 }),
Promise.all(teamsById.map(async (team) => {
const isTeamAdmin = await currentUser.hasPermission(team, "team_admin");
if (!isTeamAdmin) {
return null;
}
const teamProducts = await team.listProducts({ limit: 1 });
const hasTeamProducts = teamProducts.some((product) => product.customerType === "team");
return hasTeamProducts ? team.id : null;
})),
]);
if (cancelled) {
return;
}
const userHasProducts = userProducts.some((product) => product.customerType === "user");
const teamIdsWithProducts = new Set<string>(teamsWithProducts.filter((id): id is string => id !== null));
setPaymentsAvailability({
userHasProducts,
teamIdsWithProducts,
isReady: true,
});
});
return () => {
cancelled = true;
};
}, [props.mockUser, teamsById, userId]);
const teamsWithProducts = useMemo(
() => teamsById.filter(team => paymentsAvailability.teamIdsWithProducts.has(team.id)),
[paymentsAvailability.teamIdsWithProducts, teamsById],
);
const shouldShowPaymentsTab = props.mockUser
|| (paymentsAvailability.isReady
&& (paymentsAvailability.userHasProducts || teamsWithProducts.length > 0));
// If we're not in mock mode and don't have a user, the useUser hook will handle redirect
if (!props.mockUser && !userFromHook) {
@ -142,15 +206,19 @@ export function AccountSettings(props: {
<ApiKeysPage mockApiKeys={props.mockApiKeys} mockMode={!!props.mockUser} />
</Suspense>,
}] as const : []),
{
...(shouldShowPaymentsTab ? [{
title: t('Payments'),
type: 'item',
id: 'payments',
icon: <Icon name="CreditCard" />,
content: <Suspense fallback={<PaymentsPageSkeleton/>}>
<PaymentsPage mockMode={!!props.mockUser} />
<PaymentsPage
mockMode={!!props.mockUser}
allowPersonal={paymentsAvailability.userHasProducts}
availableTeams={teamsWithProducts}
/>
</Suspense>,
},
}] as const : []),
{
title: t('Settings'),
type: 'item',

View File

@ -1,20 +1,35 @@
'use client';
import { useState } from "react";
import { useEffect, useState } from "react";
import { Team, TeamSwitcher } from "../../..";
import { useUser } from "../../../lib/hooks";
import { useTranslation } from "../../../lib/translations";
import { PageLayout } from "../page-layout";
import { PaymentsPanel } from "./payments-panel";
export function PaymentsPage(props: { mockMode?: boolean }) {
export function PaymentsPage(props: { mockMode?: boolean, availableTeams?: Team[], allowPersonal?: boolean }) {
const { t } = useTranslation();
const user = useUser({ or: props.mockMode ? "return-null" : "redirect" });
const teams = user?.useTeams() ?? [];
const teams = props.availableTeams ?? user?.useTeams() ?? [];
const allowPersonal = props.allowPersonal ?? true;
const hasTeams = teams.length > 0;
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null);
const customer = selectedTeam ?? user;
const customerType = selectedTeam ? "team" : "user";
const effectiveSelectedTeam = selectedTeam ?? (!allowPersonal ? (teams[0] ?? null) : null);
const customer = effectiveSelectedTeam ?? (allowPersonal ? user : null);
const customerType = effectiveSelectedTeam ? "team" : "user";
useEffect(() => {
if (props.mockMode) {
return;
}
if (!allowPersonal && !selectedTeam && teams.length > 0) {
setSelectedTeam(teams[0]);
return;
}
if (selectedTeam && !teams.some(team => team.id === selectedTeam.id)) {
setSelectedTeam(allowPersonal ? null : (teams[0] ?? null));
}
}, [allowPersonal, props.mockMode, selectedTeam, teams]);
if (props.mockMode) {
return (
@ -35,8 +50,9 @@ export function PaymentsPage(props: { mockMode?: boolean }) {
<PageLayout>
{hasTeams ? (
<TeamSwitcher
team={selectedTeam ?? undefined}
allowNull
team={effectiveSelectedTeam ?? undefined}
teams={teams}
allowNull={allowPersonal}
nullLabel={t("Personal")}
onChange={async (team) => {
setSelectedTeam(team);

View File

@ -30,6 +30,7 @@ type MockTeam = {
type TeamSwitcherProps<AllowNull extends boolean = false> = {
team?: Team,
teamId?: string,
teams?: Team[],
allowNull?: AllowNull,
nullLabel?: string,
triggerClassName?: string,
@ -76,7 +77,7 @@ function Inner<AllowNull extends boolean>(props: TeamSwitcherProps<AllowNull>) {
const navigate = app.useNavigate();
const project = app.useProject();
const rawTeams = user?.useTeams();
const rawTeams = props.teams ?? user?.useTeams();
const selectedTeam = props.team || rawTeams?.find(team => team.id === props.teamId);
const teams = useMemo(() => rawTeams?.sort((a, b) => b.id === selectedTeam?.id ? 1 : -1), [rawTeams, selectedTeam]);