mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
account payments tab fix (#1147)
This commit is contained in:
parent
0f8b23dda1
commit
0f6ae49cd8
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user