Remove billing and checkout redirect buttons and related services

Deleted the billing-redirect-button, checkout-redirect-button, and embedded-stripe-checkout components. Additionally, removed the shadcn directory, which encompassed billing-related icons. This change streamlines the subscription settings interface and organizes the system's payment management. This update is a stepping stone towards improving the billing system's overall architecture.
This commit is contained in:
giancarlo
2024-03-25 11:39:41 +08:00
parent 78c704e54d
commit cb8b23e8c0
123 changed files with 1674 additions and 3071 deletions

View File

@@ -1,9 +1,9 @@
import { notFound } from 'next/navigation';
import isUserSuperAdmin from '~/admin/utils/is-user-super-admin';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import isUserSuperAdmin from '~/admin/utils/is-user-super-admin';
export function withAdminSession<Args extends unknown[], Response>(
fn: (...params: Args) => Response,
) {

View File

@@ -3,11 +3,11 @@
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { withAdminSession } from '~/admin/lib/actions-utils';
import { Logger } from '@kit/shared/logger';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { withAdminSession } from '~/admin/lib/actions-utils';
const getClient = () => getSupabaseServerActionClient({ admin: true });
export const deleteOrganizationAction = withAdminSession(

View File

@@ -4,6 +4,8 @@ import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import type Organization from '@/lib/organizations/types/organization';
import useCsrfToken from '@kit/hooks/use-csrf-token';
import { Button } from '@kit/ui/button';
import {
@@ -15,8 +17,6 @@ import {
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import type Organization from '@/lib/organizations/types/organization';
import { deleteOrganizationAction } from '../actions.server';
function DeleteOrganizationModal({

View File

@@ -1,7 +1,7 @@
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import { getOrganizationByUid } from '@/lib/organizations/database/queries';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import AdminGuard from '../../../../../../packages/admin/components/AdminGuard';
import DeleteOrganizationModal from '../components/DeleteOrganizationModal';

View File

@@ -3,6 +3,8 @@
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { DataTable } from '@/components/app/DataTable';
import type Membership from '@/lib/organizations/types/membership';
import type { ColumnDef } from '@tanstack/react-table';
import { EllipsisVerticalIcon } from 'lucide-react';
@@ -15,10 +17,6 @@ import {
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import type Membership from '@/lib/organizations/types/membership';
import { DataTable } from '@/components/app/DataTable';
import RoleBadge from '../../../../../(app)/[account]/account/organization/components/RoleBadge';
type Data = {

View File

@@ -2,15 +2,13 @@ import { use } from 'react';
import Link from 'next/link';
import { PageBody } from '@/components/app/Page';
import appConfig from '@/config/app.config';
import { ChevronRightIcon } from 'lucide-react';
import AdminHeader from '@packages/admin/components/AdminHeader';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import appConfig from '@/config/app.config';
import { PageBody } from '@/components/app/Page';
import getPageFromQueryParams from '../../../utils/get-page-from-query-param';
import { getMembershipsByOrganizationUid } from '../../queries';
import OrganizationsMembersTable from './components/OrganizationsMembersTable';

View File

@@ -1,198 +0,0 @@
'use client';
import Link from 'next/link';
import type { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon } from 'lucide-react';
import { getI18n } from 'react-i18next';
import { Button } from '@kit/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import pricingConfig from '@/config/pricing.config';
import { DataTable } from '@/components/app/DataTable';
import SubscriptionStatusBadge from '../../../(app)/[account]/components/organizations/SubscriptionStatusBadge';
import type { getOrganizations } from '../queries';
type Response = Awaited<ReturnType<typeof getOrganizations>>;
type Organizations = Response['organizations'];
const columns: ColumnDef<Organizations[0]>[] = [
{
header: 'ID',
accessorKey: 'id',
id: 'id',
size: 10,
},
{
header: 'UUID',
accessorKey: 'uuid',
id: 'uuid',
size: 200,
},
{
header: 'Name',
accessorKey: 'name',
id: 'name',
},
{
header: 'Subscription',
id: 'subscription',
cell: ({ row }) => {
const priceId = row.original?.subscription?.data?.priceId;
const plan = pricingConfig.products.find((product) => {
return product.plans.some((plan) => plan.stripePriceId === priceId);
});
if (plan) {
const price = plan.plans.find((plan) => plan.stripePriceId === priceId);
if (!price) {
return 'Unknown Price';
}
return `${plan.name} - ${price.name}`;
}
return '-';
},
},
{
header: 'Subscription Status',
id: 'subscription-status',
cell: ({ row }) => {
const subscription = row.original?.subscription?.data;
if (!subscription) {
return '-';
}
return <SubscriptionStatusBadge subscription={subscription} />;
},
},
{
header: 'Subscription Period',
id: 'subscription-period',
cell: ({ row }) => {
const subscription = row.original?.subscription?.data;
const i18n = getI18n();
const language = i18n.language ?? 'en';
if (!subscription) {
return '-';
}
const canceled = subscription.cancelAtPeriodEnd;
const date = subscription.periodEndsAt;
const formattedDate = new Date(date).toLocaleDateString(language);
return canceled ? (
<span className={'text-orange-500'}>Stops on {formattedDate}</span>
) : (
<span className={'text-green-500'}>Renews on {formattedDate}</span>
);
},
},
{
header: 'Members',
id: 'members',
cell: ({ row }) => {
const memberships = row.original.memberships.filter((item) => !item.code);
const invites = row.original.memberships.length - memberships.length;
const uid = row.original.uuid;
const length = memberships.length;
return (
<Link
data-test={'organization-members-link'}
href={`organizations/${uid}/members`}
className={'cursor-pointer hover:underline'}
>
{length} member{length === 1 ? '' : 's'}{' '}
{invites ? `(${invites} invites)` : ''}
</Link>
);
},
},
{
header: '',
id: 'actions',
cell: ({ row }) => {
const organization = row.original;
const uid = organization.uuid;
return (
<div className={'flex justify-end'}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size={'icon'}>
<span className="sr-only">Open menu</span>
<EllipsisIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(uid)}
>
Copy UUID
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/admin/organizations/${uid}/members`}>
View Members
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
className={'text-red-500'}
href={`/admin/organizations/${uid}/delete`}
>
Delete
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];
function OrganizationsTable({
organizations,
pageCount,
perPage,
page,
}: React.PropsWithChildren<{
organizations: Organizations;
pageCount: number;
perPage: number;
page: number;
}>) {
return (
<DataTable
tableProps={{
'data-test': 'admin-organizations-table',
}}
pageSize={perPage}
pageIndex={page - 1}
pageCount={pageCount}
columns={columns}
data={organizations}
/>
);
}
export default OrganizationsTable;

View File

@@ -1,9 +1,9 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { PageBody } from '@/components/app/Page';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
function OrganizationsAdminPageError() {
return (
<PageBody>

View File

@@ -1,3 +1,5 @@
import { PageBody } from '@/components/app/Page';
import appConfig from '@/config/app.config';
import AdminGuard from '@/packages/admin/components/AdminGuard';
import AdminHeader from '@/packages/admin/components/AdminHeader';
@@ -5,10 +7,6 @@ import getSupabaseServerComponentClient from '@packages/supabase/server-componen
import { Input } from '@kit/ui/input';
import appConfig from '@/config/app.config';
import { PageBody } from '@/components/app/Page';
import OrganizationsTable from './components/OrganizationsTable';
import { getOrganizations } from './queries';

View File

@@ -1,7 +1,6 @@
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@/database.types';
import { MEMBERSHIPS_TABLE, ORGANIZATIONS_TABLE } from '@/lib/db-tables';
import type { UserOrganizationData } from '@/lib/organizations/database/queries';
import type MembershipRole from '@/lib/organizations/types/membership-role';

View File

@@ -1,8 +1,7 @@
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import { PageBody } from '@/components/app/Page';
import appConfig from '@/config/app.config';
import { PageBody } from '@/components/app/Page';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import AdminDashboard from '../../packages/admin/components/AdminDashboard';
import AdminGuard from '../../packages/admin/components/AdminGuard';

View File

@@ -3,11 +3,11 @@
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { withAdminSession } from '~/admin/lib/actions-utils';
import { Logger } from '@kit/shared/logger';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { withAdminSession } from '~/admin/lib/actions-utils';
const getClient = () => getSupabaseServerActionClient({ admin: true });
export const banUser = withAdminSession(async ({ userId }) => {

View File

@@ -8,6 +8,8 @@ import { useRouter } from 'next/navigation';
import type { User } from '@supabase/gotrue-js';
import ErrorBoundary from '@/components/app/ErrorBoundary';
import useCsrfToken from '@kit/hooks/use-csrf-token';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
@@ -20,8 +22,6 @@ import {
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import ErrorBoundary from '@/components/app/ErrorBoundary';
import { banUser } from '../actions.server';
function BanUserModal({

View File

@@ -4,10 +4,10 @@ import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import useSupabase from '@kit/hooks/use-supabase';
import Spinner from '@/components/app/Spinner';
import useSupabase from '@kit/hooks/use-supabase';
function ImpersonateUserAuthSetter({
tokens,
}: React.PropsWithChildren<{

View File

@@ -6,6 +6,9 @@ import { useRouter } from 'next/navigation';
import type { User } from '@supabase/gotrue-js';
import If from '@/components/app/If';
import LoadingOverlay from '@/components/app/LoadingOverlay';
import useCsrfToken from '@kit/hooks/use-csrf-token';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
@@ -16,9 +19,6 @@ import {
DialogTitle,
} from '@kit/ui/dialog';
import If from '@/components/app/If';
import LoadingOverlay from '@/components/app/LoadingOverlay';
import { impersonateUser } from '../actions.server';
import ImpersonateUserAuthSetter from '../components/ImpersonateUserAuthSetter';

View File

@@ -2,6 +2,7 @@
import Link from 'next/link';
import If from '@/components/app/If';
import { EllipsisVerticalIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
@@ -12,8 +13,6 @@ import {
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import If from '@/components/app/If';
function UserActionsDropdown({
uid,
isBanned,

View File

@@ -1,5 +1,8 @@
import Link from 'next/link';
import { PageBody } from '@/components/app/Page';
import configuration from '@/config/app.config';
import type MembershipRole from '@/lib/organizations/types/membership-role';
import { ChevronRightIcon } from 'lucide-react';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
@@ -17,12 +20,6 @@ import {
TableRow,
} from '@kit/ui/table';
import configuration from '@/config/app.config';
import type MembershipRole from '@/lib/organizations/types/membership-role';
import { PageBody } from '@/components/app/Page';
import RoleBadge from '../../../(app)/[account]/account/organization/components/RoleBadge';
import AdminGuard from '../../../../packages/admin/components/AdminGuard';
import AdminHeader from '../../../../packages/admin/components/AdminHeader';

View File

@@ -1,239 +0,0 @@
'use client';
import Link from 'next/link';
import type { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon } from 'lucide-react';
import { getI18n } from 'react-i18next';
import type UserData from '@kit/session/types/user-data';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kit/ui/tooltip';
import { DataTable } from '@/components/app/DataTable';
import If from '@/components/app/If';
type UserRow = {
id: string;
email: string | undefined;
phone: string | undefined;
createdAt: string;
updatedAt: string | undefined;
lastSignInAt: string | undefined;
banDuration: string | undefined;
data: UserData;
};
const columns: ColumnDef<UserRow>[] = [
{
header: '',
id: 'avatar',
size: 10,
cell: ({ row }) => {
const user = row.original;
const data = user.data;
const displayName = data?.displayName;
const photoUrl = data?.photoUrl;
const displayText = displayName ?? user.email ?? user.phone ?? '';
return (
<Tooltip>
<TooltipTrigger>
<Avatar>
{photoUrl ? <AvatarImage src={photoUrl} /> : null}
<AvatarFallback>{displayText[0]}</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent>{displayText}</TooltipContent>
</Tooltip>
);
},
},
{
header: 'ID',
id: 'id',
size: 30,
cell: ({ row }) => {
const id = row.original.id;
return (
<Link className={'hover:underline'} href={`/admin/users/${id}`}>
{id}
</Link>
);
},
},
{
header: 'Email',
id: 'email',
cell: ({ row }) => {
const email = row.original.email;
return (
<span title={email} className={'block max-w-full truncate'}>
{email}
</span>
);
},
},
{
header: 'Name',
size: 50,
id: 'displayName',
cell: ({ row }) => {
return row.original.data?.displayName ?? '';
},
},
{
header: 'Created at',
id: 'createdAt',
cell: ({ row }) => {
const date = new Date(row.original.createdAt);
const i18n = getI18n();
const language = i18n.language ?? 'en';
const createdAtLabel = date.toLocaleDateString(language);
return <span>{createdAtLabel}</span>;
},
},
{
header: 'Last sign in',
id: 'lastSignInAt',
cell: ({ row }) => {
const lastSignInAt = row.original.lastSignInAt;
if (!lastSignInAt) {
return <span>-</span>;
}
const date = new Date(lastSignInAt);
return <span suppressHydrationWarning>{date.toLocaleString()}</span>;
},
},
{
header: 'Status',
id: 'status',
cell: ({ row }) => {
const banDuration = row.original.banDuration;
if (!banDuration || banDuration === 'none') {
return (
<Badge className={'inline-flex'} color={'success'}>
Active
</Badge>
);
}
return (
<Badge className={'inline-flex'} color={'error'}>
Banned
</Badge>
);
},
},
{
header: '',
id: 'actions',
cell: ({ row }) => {
const user = row.original;
const banDuration = row.original.banDuration;
const isBanned = banDuration && banDuration !== 'none';
return (
<div className={'flex justify-end'}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size={'icon'}>
<span className="sr-only">Open menu</span>
<EllipsisIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(user.id)}
>
Copy user ID
</DropdownMenuItem>
<If condition={!isBanned}>
<DropdownMenuItem asChild>
<Link href={`/admin/users/${user.id}/impersonate`}>
Impersonate User
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
className={
'text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-500/5'
}
href={`/admin/users/${user.id}/ban`}
>
Ban User
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
className={
'text-red-500 hover:bg-red-50 dark:hover:bg-red-500/5'
}
href={`/admin/users/${user.id}/delete`}
>
Delete User
</Link>
</DropdownMenuItem>
</If>
<If condition={isBanned}>
<DropdownMenuItem asChild>
<Link href={`/admin/users/${user.id}/reactivate`}>
Reactivate User
</Link>
</DropdownMenuItem>
</If>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];
function UsersTable({
users,
page,
pageCount,
perPage,
}: React.PropsWithChildren<{
users: UserRow[];
pageCount: number;
page: number;
perPage: number;
}>) {
return (
<DataTable
tableProps={{
'data-test': 'admin-users-table',
}}
pageIndex={page - 1}
pageSize={perPage}
pageCount={pageCount}
data={users}
columns={columns}
/>
);
}
export default UsersTable;

View File

@@ -1,9 +1,9 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { PageBody } from '@/components/app/Page';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
function UsersAdminPageError() {
return (
<PageBody>

View File

@@ -1,15 +1,13 @@
import { PageBody } from '@/components/app/Page';
import appConfig from '@/config/app.config';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import type UserData from '@kit/session/types/user-data';
import appConfig from '@/config/app.config';
import { PageBody } from '@/components/app/Page';
import AdminGuard from '../../../packages/admin/components/AdminGuard';
import AdminHeader from '../../../packages/admin/components/AdminHeader';
import getPageFromQueryParams from '../utils/get-page-from-query-param';
import UsersTable from './components/UsersTable';
import { getUsers } from './queries';
interface UsersAdminPageProps {
@@ -32,14 +30,7 @@ async function UsersAdminPage({ searchParams }: UsersAdminPageProps) {
<div className={'flex flex-1 flex-col'}>
<AdminHeader>Users</AdminHeader>
<PageBody>
<UsersTable
users={users}
page={page}
pageCount={pageCount}
perPage={perPage}
/>
</PageBody>
<PageBody></PageBody>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import { USERS_TABLE } from '@/lib/db-tables';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
export async function getUsers(ids: string[]) {
const client = getSupabaseServerComponentClient({ admin: true });