diff --git a/apps/web/.env.development b/apps/web/.env.development index 44a227b8b..060ca404a 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -4,7 +4,6 @@ NEXT_PUBLIC_PRODUCT_NAME=Makerkit # SUPABASE NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 -SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU NEXT_PUBLIC_REQUIRE_EMAIL_CONFIRMATION=true @@ -32,5 +31,5 @@ NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales # Please make sure to update these in the app's paths configuration as well SIGN_IN_PATH=/auth/sign-in SIGN_UP_PATH=/auth/sign-up -ORGANIZATION_ACCOUNTS_PATH=/home +TEAM_ACCOUNTS_HOME_PATH=/home INVITATION_PAGE_PATH=/join \ No newline at end of file diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx b/apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx index 9508394bd..cd22afa92 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx @@ -2,7 +2,10 @@ import { useState, useTransition } from 'react'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; + import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components'; +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Card, CardContent, @@ -10,6 +13,7 @@ import { CardHeader, CardTitle, } from '@kit/ui/card'; +import { If } from '@kit/ui/if'; import billingConfig from '~/config/billing.config'; @@ -17,7 +21,8 @@ import { createPersonalAccountCheckoutSession } from '../server-actions'; export function PersonalAccountCheckoutForm() { const [pending, startTransition] = useTransition(); - const [checkoutToken, setCheckoutToken] = useState(null); + const [error, setError] = useState(false); + const [checkoutToken, setCheckoutToken] = useState(); // If the checkout token is set, render the embedded checkout component if (checkoutToken) { @@ -41,18 +46,26 @@ export function PersonalAccountCheckoutForm() { - + + + + + { startTransition(async () => { - const { checkoutToken } = - await createPersonalAccountCheckoutSession({ - planId, - }); + try { + const { checkoutToken } = + await createPersonalAccountCheckoutSession({ + planId, + }); - setCheckoutToken(checkoutToken); + setCheckoutToken(checkoutToken); + } catch (e) { + setError(true); + } }); }} /> @@ -61,3 +74,17 @@ export function PersonalAccountCheckoutForm() { ); } + +function ErrorAlert() { + return ( + + + + Sorry, we encountered an error. + + + We couldn't process your request. Please try again. + + + ); +} diff --git a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx index 8f9460730..51135d1c0 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx @@ -1,10 +1,37 @@ +import { redirect } from 'next/navigation'; + +import { SupabaseClient } from '@supabase/supabase-js'; + +import { + BillingPortalCard, + CurrentPlanCard, +} from '@kit/billing-gateway/components'; +import { Database } from '@kit/supabase/database'; +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { If } from '@kit/ui/if'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; -import { PersonalAccountCheckoutForm } from '~/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form'; +import { createPersonalAccountBillingPortalSession } from '~/(dashboard)/home/(user)/billing/server-actions'; +import billingConfig from '~/config/billing.config'; +import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -function PersonalAccountBillingPage() { +import { loadUserWorkspace } from '../../_lib/load-user-workspace'; +import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form'; + +type Subscription = Database['public']['Tables']['subscriptions']['Row']; + +async function PersonalAccountBillingPage() { + const client = getSupabaseServerComponentClient(); + const { session } = await loadUserWorkspace(); + + if (!session?.user) { + redirect(pathsConfig.auth.signIn); + } + + const [subscription, customerId] = await loadData(client, session.user.id); + return ( <> - +
+
+ } + > + {(subscription) => ( + + )} + + + +
+ + +
+
+
); } export default withI18n(PersonalAccountBillingPage); + +function loadData(client: SupabaseClient, userId: string) { + const subscription = client + .from('subscriptions') + .select('*') + .eq('account_id', userId) + .maybeSingle() + .then(({ data }) => data); + + const customer = client + .from('billing_customers') + .select('customer_id') + .eq('account_id', userId) + .maybeSingle() + .then(({ data }) => data?.customer_id); + + return Promise.all([subscription, customer]); +} diff --git a/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx index 6fbf074e1..9f2927a16 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx @@ -1,3 +1,5 @@ // We reuse the page from the billing module // as there is no need to create a new one. -export * from '../return/page'; +import ReturnStripeSessionPage from '../../../[account]/billing/return/page'; + +export default ReturnStripeSessionPage; diff --git a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts index d4c34fb60..b0c81ca13 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts @@ -1,14 +1,16 @@ 'use server'; -import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { z } from 'zod'; import { getProductPlanPairFromId } from '@kit/billing'; import { getBillingGatewayProvider } from '@kit/billing-gateway'; +import { Logger } from '@kit/shared/logger'; +import { requireAuth } from '@kit/supabase/require-auth'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; @@ -22,13 +24,21 @@ export async function createPersonalAccountCheckoutSession(params: { planId: string; }) { const client = getSupabaseServerActionClient(); - const { data, error } = await client.auth.getUser(); + const { data, error } = await requireAuth(client); if (error ?? !data.user) { throw new Error('Authentication required'); } const planId = z.string().min(1).parse(params.planId); + + Logger.info( + { + planId, + }, + `Creating checkout session for plan ID`, + ); + const service = await getBillingGatewayProvider(client); const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId); @@ -61,6 +71,13 @@ export async function createPersonalAccountCheckoutSession(params: { customerId, }); + Logger.info( + { + userId: data.user.id, + }, + `Checkout session created. Returning checkout token to client...`, + ); + // return the checkout token to the client // so we can call the payment gateway to complete the checkout return { @@ -68,7 +85,7 @@ export async function createPersonalAccountCheckoutSession(params: { }; } -export async function createBillingPortalSession() { +export async function createPersonalAccountBillingPortalSession() { const client = getSupabaseServerActionClient(); const { data, error } = await client.auth.getUser(); @@ -95,18 +112,17 @@ export async function createBillingPortalSession() { } function getCheckoutSessionReturnUrl() { - const origin = headers().get('origin')!; - return new URL( pathsConfig.app.personalAccountBillingReturn, - origin, + appConfig.url, ).toString(); } function getBillingPortalReturnUrl() { - const origin = headers().get('origin')!; - - return new URL(pathsConfig.app.accountBilling, origin).toString(); + return new URL( + pathsConfig.app.personalAccountBilling, + appConfig.url, + ).toString(); } async function getCustomerIdFromAccountId(accountId: string) { diff --git a/apps/web/app/(dashboard)/home/(user)/settings/loading.tsx b/apps/web/app/(dashboard)/home/(user)/settings/loading.tsx deleted file mode 100644 index 4ea53181d..000000000 --- a/apps/web/app/(dashboard)/home/(user)/settings/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { GlobalLoader } from '@kit/ui/global-loader'; - -export default GlobalLoader; diff --git a/apps/web/app/(dashboard)/home/(user)/settings/page.tsx b/apps/web/app/(dashboard)/home/(user)/settings/page.tsx index cac56ae21..b49be99fd 100644 --- a/apps/web/app/(dashboard)/home/(user)/settings/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/settings/page.tsx @@ -4,6 +4,14 @@ import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; +const features = { + enableAccountDeletion: featureFlagsConfig.enableAccountDeletion, +}; + +const paths = { + callback: pathsConfig.auth.callback, +}; + function PersonalAccountSettingsPage() { return (
- +
); } diff --git a/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx index 6de999cca..ef7fd5fc9 100644 --- a/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation'; import type { Session } from '@supabase/supabase-js'; -import { ArrowLeftCircleIcon, ArrowRightCircleIcon } from 'lucide-react'; +import { ArrowLeftCircle, ArrowRightCircle } from 'lucide-react'; import { AccountSelector } from '@kit/accounts/account-selector'; import { Sidebar, SidebarContent } from '@kit/ui/sidebar'; @@ -138,13 +138,13 @@ function CollapsibleButton({ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'} onClick={() => onClick(!collapsed)} > - - { switch (props.trend) { case 'up': - return ; + return ; case 'down': - return ; + return ; case 'stale': - return ; + return ; } }, [props.trend]); diff --git a/apps/web/app/(dashboard)/home/[account]/_components/mobile-app-navigation.tsx b/apps/web/app/(dashboard)/home/[account]/_components/mobile-app-navigation.tsx index 7e65eafcc..106a8be1e 100644 --- a/apps/web/app/(dashboard)/home/[account]/_components/mobile-app-navigation.tsx +++ b/apps/web/app/(dashboard)/home/[account]/_components/mobile-app-navigation.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { HomeIcon, LogOutIcon, MenuIcon } from 'lucide-react'; +import { Home, LogOut, Menu } from 'lucide-react'; import { AccountSelector } from '@kit/accounts/account-selector'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; @@ -67,7 +67,7 @@ export const MobileAppNavigation = ( return ( - + @@ -115,7 +115,7 @@ function SignOutDropdownItem( className={'flex h-12 w-full items-center space-x-4'} onClick={props.onSignOut} > - + @@ -135,7 +135,7 @@ function OrganizationsModal() { onSelect={(e) => e.preventDefault()} > - - - Visit the billing portal to manage your subscription (update - payment method, cancel subscription, etc.) - - - - - - ); -} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx b/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx index 4ece16b65..dc39b3cce 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx @@ -2,6 +2,8 @@ import { useState, useTransition } from 'react'; +import { useParams } from 'next/navigation'; + import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components'; import { Card, @@ -16,6 +18,7 @@ import billingConfig from '~/config/billing.config'; import { createTeamAccountCheckoutSession } from '../server-actions'; export function TeamAccountCheckoutForm(params: { accountId: string }) { + const routeParams = useParams(); const [pending, startTransition] = useTransition(); const [checkoutToken, setCheckoutToken] = useState(null); @@ -31,34 +34,32 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) { // Otherwise, render the plan picker component return ( -
- - - Manage your Team Plan + + + Manage your Team Plan - - You can change your plan at any time. - - + You can change your plan at any time. + - - { - startTransition(async () => { - const { checkoutToken } = - await createTeamAccountCheckoutSession({ - planId, - accountId: params.accountId, - }); + + { + startTransition(async () => { + const slug = routeParams.account as string; - setCheckoutToken(checkoutToken); + const { checkoutToken } = await createTeamAccountCheckoutSession({ + planId, + accountId: params.accountId, + slug, }); - }} - /> - - -
+ + setCheckoutToken(checkoutToken); + }); + }} + /> + + ); } diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 192851602..1fd600aab 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -1,10 +1,16 @@ +import { + BillingPortalCard, + CurrentPlanCard, +} from '@kit/billing-gateway/components'; +import { Database } from '@kit/supabase/database'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { If } from '@kit/ui/if'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace'; -import { BillingPortalForm } from '~/(dashboard)/home/[account]/billing/_components/billing-portal-form'; +import { createBillingPortalSession } from '~/(dashboard)/home/[account]/billing/server-actions'; +import billingConfig from '~/config/billing.config'; import { withI18n } from '~/lib/i18n/with-i18n'; import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form'; @@ -18,7 +24,7 @@ interface Params { async function OrganizationAccountBillingPage({ params }: Params) { const workspace = await loadOrganizationWorkspace(params.account); const accountId = workspace.account.id; - const customerId = await loadCustomerIdFromAccount(accountId); + const [subscription, customerId] = await loadAccountData(accountId); return ( <> @@ -28,11 +34,27 @@ async function OrganizationAccountBillingPage({ params }: Params) { /> - +
+
+ } + > + {(data) => ( + + )} + - - - + +
+ + + + + +
+
+
); @@ -40,18 +62,21 @@ async function OrganizationAccountBillingPage({ params }: Params) { export default withI18n(OrganizationAccountBillingPage); -async function loadCustomerIdFromAccount(accountId: string) { +async function loadAccountData(accountId: string) { const client = getSupabaseServerComponentClient(); - const { data, error } = await client + const subscription = client + .from('subscriptions') + .select('*') + .eq('account_id', accountId) + .maybeSingle() + .then(({ data }) => data); + + const customerId = client .from('billing_customers') .select('customer_id') .eq('account_id', accountId) .maybeSingle(); - if (error) { - throw error; - } - - return data?.customer_id; + return Promise.all([subscription, customerId]); } diff --git a/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts b/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts index 65f32d1d4..3c1701e11 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts @@ -1,6 +1,5 @@ 'use server'; -import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { z } from 'zod'; @@ -10,6 +9,7 @@ import { getBillingGatewayProvider } from '@kit/billing-gateway'; import { requireAuth } from '@kit/supabase/require-auth'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; @@ -22,6 +22,7 @@ import pathsConfig from '~/config/paths.config'; export async function createTeamAccountCheckoutSession(params: { planId: string; accountId: string; + slug: string; }) { const client = getSupabaseServerActionClient(); @@ -57,7 +58,7 @@ export async function createTeamAccountCheckoutSession(params: { } // the return URL for the checkout session - const returnUrl = getCheckoutSessionReturnUrl(); + const returnUrl = getCheckoutSessionReturnUrl(params.slug); // find the customer ID for the account if it exists // (eg. if the account has been billed before) @@ -85,14 +86,15 @@ export async function createTeamAccountCheckoutSession(params: { }; } -export async function createBillingPortalSession(data: FormData) { +export async function createBillingPortalSession(formData: FormData) { const client = getSupabaseServerActionClient(); - const accountId = z + const { accountId, slug } = z .object({ accountId: z.string().min(1), + slug: z.string().min(1), }) - .parse(Object.fromEntries(data)).accountId; + .parse(Object.fromEntries(formData)); const { data: session, error } = await requireAuth(client); @@ -113,7 +115,7 @@ export async function createBillingPortalSession(data: FormData) { const service = await getBillingGatewayProvider(client); const customerId = await getCustomerIdFromAccountId(client, accountId); - const returnUrl = getBillingPortalReturnUrl(); + const returnUrl = getBillingPortalReturnUrl(slug); if (!customerId) { throw new Error('Customer not found'); @@ -128,16 +130,16 @@ export async function createBillingPortalSession(data: FormData) { return redirect(url); } -function getCheckoutSessionReturnUrl() { - const origin = headers().get('origin')!; - - return new URL(pathsConfig.app.accountBillingReturn, origin).toString(); +function getCheckoutSessionReturnUrl(accountSlug: string) { + return new URL(pathsConfig.app.accountBillingReturn, appConfig.url) + .toString() + .replace('[account]', accountSlug); } -function getBillingPortalReturnUrl() { - const origin = headers().get('origin')!; - - return new URL(pathsConfig.app.accountBilling, origin).toString(); +function getBillingPortalReturnUrl(accountSlug: string) { + return new URL(pathsConfig.app.accountBilling, appConfig.url) + .toString() + .replace('[account]', accountSlug); } /** diff --git a/apps/web/app/(dashboard)/home/[account]/members/page.tsx b/apps/web/app/(dashboard)/home/[account]/members/page.tsx index 01e5644b9..2ba595b87 100644 --- a/apps/web/app/(dashboard)/home/[account]/members/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/members/page.tsx @@ -1,4 +1,4 @@ -import { PlusCircleIcon } from 'lucide-react'; +import { PlusCircle } from 'lucide-react'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { @@ -99,7 +99,7 @@ async function OrganizationAccountMembersPage({ params }: Params) { diff --git a/apps/web/app/(dashboard)/home/[account]/page.tsx b/apps/web/app/(dashboard)/home/[account]/page.tsx index 33bc58cf1..820e36cea 100644 --- a/apps/web/app/(dashboard)/home/[account]/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/page.tsx @@ -1,6 +1,6 @@ import loadDynamic from 'next/dynamic'; -import { PlusIcon } from 'lucide-react'; +import { Plus } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { PageBody } from '@kit/ui/page'; @@ -11,7 +11,7 @@ import { AppHeader } from '~/(dashboard)/home/[account]/_components/app-header'; import { withI18n } from '~/lib/i18n/with-i18n'; const DashboardDemo = loadDynamic( - () => import('~/(dashboard)/home/[account]/_components/dashboard-demo'), + () => import('./_components/dashboard-demo'), { ssr: false, loading: () => ( @@ -50,7 +50,7 @@ function OrganizationAccountHomePage({ account={params.account} > diff --git a/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx b/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx index 80bb61a6f..77c2678dd 100644 --- a/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx @@ -2,19 +2,16 @@ import { use } from 'react'; import { cookies } from 'next/headers'; -import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; import { HomeSidebarAccountSelector } from '~/(dashboard)/home/_components/home-sidebar-account-selector'; import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown'; +import { loadUserWorkspace } from '~/(dashboard)/home/_lib/load-user-workspace'; import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config'; export function HomeSidebar() { const collapsed = getSidebarCollapsed(); - - const [accounts, session] = use( - Promise.all([loadUserAccounts(), loadSession()]), - ); + const { session, accounts } = use(loadUserWorkspace()); return ( @@ -38,38 +35,3 @@ export function HomeSidebar() { function getSidebarCollapsed() { return cookies().get('sidebar-collapsed')?.value === 'true'; } - -async function loadSession() { - const client = getSupabaseServerComponentClient(); - - const { - data: { session }, - error, - } = await client.auth.getSession(); - - if (error) { - throw error; - } - - return session; -} - -async function loadUserAccounts() { - const client = getSupabaseServerComponentClient(); - - const { data: accounts, error } = await client - .from('user_accounts') - .select(`name, slug, picture_url`); - - if (error) { - throw error; - } - - return accounts.map(({ name, slug, picture_url }) => { - return { - label: name, - value: slug, - image: picture_url, - }; - }); -} diff --git a/apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts b/apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts new file mode 100644 index 000000000..eee102a8a --- /dev/null +++ b/apps/web/app/(dashboard)/home/_lib/load-user-workspace.ts @@ -0,0 +1,50 @@ +import { cache } from 'react'; + +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +export const loadUserWorkspace = cache(async () => { + const [accounts, session] = await Promise.all([ + loadUserAccounts(), + loadSession(), + ]); + + return { + accounts, + session, + }; +}); + +async function loadSession() { + const client = getSupabaseServerComponentClient(); + + const { + data: { session }, + error, + } = await client.auth.getSession(); + + if (error) { + throw error; + } + + return session; +} + +async function loadUserAccounts() { + const client = getSupabaseServerComponentClient(); + + const { data: accounts, error } = await client + .from('user_accounts') + .select(`name, slug, picture_url`); + + if (error) { + throw error; + } + + return accounts.map(({ name, slug, picture_url }) => { + return { + label: name, + value: slug, + image: picture_url, + }; + }); +} diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx index e313b0cff..c36512ace 100644 --- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx +++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import type { Session } from '@supabase/supabase-js'; -import { ChevronRightIcon } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; @@ -60,7 +60,7 @@ function AuthButtons() { diff --git a/apps/web/app/(marketing)/_components/site-header.tsx b/apps/web/app/(marketing)/_components/site-header.tsx index cdccb4fb5..39d61e49d 100644 --- a/apps/web/app/(marketing)/_components/site-header.tsx +++ b/apps/web/app/(marketing)/_components/site-header.tsx @@ -4,7 +4,7 @@ import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header- import { SiteNavigation } from '~/(marketing)/_components/site-navigation'; import { AppLogo } from '~/components/app-logo'; -export async function SiteHeader(props: { session: Session | null }) { +export async function SiteHeader(props: { session?: Session | null }) { return (
@@ -19,7 +19,7 @@ export async function SiteHeader(props: { session: Session | null }) {
- +
diff --git a/apps/web/app/(marketing)/_components/site-navigation.tsx b/apps/web/app/(marketing)/_components/site-navigation.tsx index 4fe26e15b..8dd505e9c 100644 --- a/apps/web/app/(marketing)/_components/site-navigation.tsx +++ b/apps/web/app/(marketing)/_components/site-navigation.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { MenuIcon } from 'lucide-react'; +import { Menu } from 'lucide-react'; import { DropdownMenu, @@ -81,7 +81,7 @@ function MobileDropdown() { return ( - + diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index 0a4223cae..0a2417aaa 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -3,7 +3,7 @@ import { cache } from 'react'; import { notFound } from 'next/navigation'; import { allDocumentationPages } from 'contentlayer/generated'; -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; import { If } from '@kit/ui/if'; import { Mdx } from '@kit/ui/mdx'; @@ -77,7 +77,7 @@ function DocumentationPage({ params }: PageParams) { {(page) => ( } + before={} /> )} @@ -88,7 +88,7 @@ function DocumentationPage({ params }: PageParams) { {(page) => ( } + after={} /> )} diff --git a/apps/web/app/(marketing)/docs/_components/docs-card.tsx b/apps/web/app/(marketing)/docs/_components/docs-card.tsx index e57733685..a6b4836b9 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-card.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-card.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { ChevronRightIcon } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; export const DocsCard: React.FC< React.PropsWithChildren<{ @@ -36,7 +36,7 @@ export const DocsCard: React.FC< {link.label} - +
)} diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx index 1a9452662..266a7a00a 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { ChevronDownIcon, MenuIcon } from 'lucide-react'; +import { ChevronDown, Menu } from 'lucide-react'; import { isBrowser } from '@kit/shared/utils'; import { Button } from '@kit/ui/button'; @@ -53,7 +53,7 @@ const DocsNavLink: React.FC<{ - + )} @@ -218,7 +218,7 @@ function FloatingDocumentationNavigation({ className={'fixed bottom-5 right-5 z-10 h-16 w-16 rounded-full'} onClick={onClick} > - + ); diff --git a/apps/web/app/(marketing)/faq/page.tsx b/apps/web/app/(marketing)/faq/page.tsx index 312feeab5..7e1813cb1 100644 --- a/apps/web/app/(marketing)/faq/page.tsx +++ b/apps/web/app/(marketing)/faq/page.tsx @@ -1,4 +1,4 @@ -import { ChevronDownIcon } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -108,7 +108,7 @@ function FaqItem({
-
diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index 0c54e53d4..cedea559a 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import Link from 'next/link'; -import { ChevronRightIcon } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -135,7 +135,7 @@ function Home() {
@@ -273,7 +273,7 @@ function MainCallToActionButton() { Get Started - getSupabaseRouteHandlerClient({ admin: true }); + + const service = await getBillingEventHandlerService(clientProvider, provider); try { await service.handleWebhookEvent(request); diff --git a/apps/web/app/api/database/webhook.ts b/apps/web/app/api/database/webhook.ts new file mode 100644 index 000000000..efe1e2ecd --- /dev/null +++ b/apps/web/app/api/database/webhook.ts @@ -0,0 +1,3 @@ +export function POST(request: Request) { + console.log(request); +} diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index 613518207..a3e84ac15 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; -import { ArrowLeftIcon } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -41,7 +41,7 @@ const ErrorPage = () => {
diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index bbdd6a773..d41d92e3b 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { ArrowLeftIcon } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -44,7 +44,7 @@ const NotFoundPage = () => { diff --git a/apps/web/app/server-sitemap.xml/route.ts b/apps/web/app/server-sitemap.xml/route.ts index 3baae3040..e1ca73c1c 100644 --- a/apps/web/app/server-sitemap.xml/route.ts +++ b/apps/web/app/server-sitemap.xml/route.ts @@ -4,7 +4,7 @@ import { getServerSideSitemap } from 'next-sitemap'; import appConfig from '~/config/app.config'; -const siteUrl = appConfig.url; +invariant(appConfig.url, 'No NEXT_PUBLIC_SITE_URL environment variable found'); export async function GET() { const urls = getSiteUrls(); @@ -15,35 +15,29 @@ export async function GET() { } function getSiteUrls() { - invariant(siteUrl, 'No NEXT_PUBLIC_SITE_URL found'); - - const urls = ['', 'faq', 'pricing']; + const urls = ['/', 'faq', 'pricing']; return urls.map((url) => { return { - loc: new URL(siteUrl, url).href, + loc: new URL(url, appConfig.url).href, lastmod: new Date().toISOString(), }; }); } function getPostsSitemap() { - invariant(siteUrl, 'No NEXT_PUBLIC_SITE_URL found'); - return allPosts.map((post) => { return { - loc: new URL(siteUrl, post.url).href, + loc: new URL(post.url, appConfig.url).href, lastmod: new Date().toISOString(), }; }); } function getDocsSitemap() { - invariant(siteUrl, 'No NEXT_PUBLIC_SITE_URL found'); - return allDocumentationPages.map((page) => { return { - loc: new URL(siteUrl, page.url).href, + loc: new URL(page.url, appConfig.url).href, lastmod: new Date().toISOString(), }; }); diff --git a/apps/web/config/billing.config.ts b/apps/web/config/billing.config.ts index 264757bab..ba86c99a0 100644 --- a/apps/web/config/billing.config.ts +++ b/apps/web/config/billing.config.ts @@ -12,7 +12,7 @@ export default createBillingSchema({ badge: `Value`, plans: [ { - id: 'starter-monthly', + id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe', name: 'Starter Monthly', price: '9.99', interval: 'month', diff --git a/apps/web/config/organization-account-sidebar.config.tsx b/apps/web/config/organization-account-sidebar.config.tsx index 3dee549af..60fa02dce 100644 --- a/apps/web/config/organization-account-sidebar.config.tsx +++ b/apps/web/config/organization-account-sidebar.config.tsx @@ -1,9 +1,4 @@ -import { - CreditCardIcon, - LayoutDashboardIcon, - SettingsIcon, - UsersIcon, -} from 'lucide-react'; +import { CreditCard, LayoutDashboard, Settings, Users } from 'lucide-react'; import { SidebarConfigSchema } from '@kit/ui/sidebar-schema'; @@ -16,7 +11,7 @@ const routes = (account: string) => [ { label: 'common:dashboardTabLabel', path: pathsConfig.app.accountHome.replace('[account]', account), - Icon: , + Icon: , end: true, }, { @@ -26,18 +21,18 @@ const routes = (account: string) => [ { label: 'common:settingsTabLabel', path: createPath(pathsConfig.app.accountSettings, account), - Icon: , + Icon: , }, { label: 'common:accountMembers', path: createPath(pathsConfig.app.accountMembers, account), - Icon: , + Icon: , }, featureFlagsConfig.enableOrganizationBilling ? { label: 'common:billingTabLabel', path: createPath(pathsConfig.app.accountBilling, account), - Icon: , + Icon: , } : undefined, ].filter(Boolean), diff --git a/apps/web/config/personal-account-sidebar.config.tsx b/apps/web/config/personal-account-sidebar.config.tsx index 00d749763..f7df14c21 100644 --- a/apps/web/config/personal-account-sidebar.config.tsx +++ b/apps/web/config/personal-account-sidebar.config.tsx @@ -1,4 +1,4 @@ -import { CreditCardIcon, HomeIcon, UserIcon } from 'lucide-react'; +import { CreditCard, Home, User } from 'lucide-react'; import { SidebarConfigSchema } from '@kit/ui/sidebar-schema'; @@ -11,13 +11,13 @@ const routes = [ { label: 'common:homeTabLabel', path: pathsConfig.app.home, - Icon: , + Icon: , end: true, }, { label: 'common:yourAccountTabLabel', path: pathsConfig.app.personalAccountSettings, - Icon: , + Icon: , }, ]; @@ -25,7 +25,7 @@ if (featureFlagsConfig.enablePersonalAccountBilling) { routes.push({ label: 'common:billingTabLabel', path: pathsConfig.app.personalAccountBilling, - Icon: , + Icon: , }); } diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ac16d8361..01a8e745d 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -18,6 +18,7 @@ const config = { '@kit/mailers', '@kit/billing', '@kit/billing-gateway', + '@kit/stripe', ], pageExtensions: ['ts', 'tsx'], images: { @@ -25,6 +26,12 @@ const config = { }, experimental: { mdxRs: true, + optimizePackageImports: [] + }, + modularizeImports: { + "lucide-react": { + transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}", + } }, /** We already do linting and typechecking as separate tasks in CI */ eslint: { ignoreDuringBuilds: true }, diff --git a/apps/web/package.json b/apps/web/package.json index e77f2b9e9..a03eac9ca 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "analyze": "ANALYZE=true pnpm run build", "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", - "dev": "pnpm with-env next dev --turbo", + "dev": "pnpm with-env next dev", "lint": "next lint", "format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"", "start": "pnpm with-env next start", diff --git a/package.json b/package.json index 1fcbbe1b8..f27fb780c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "lint": "turbo lint --continue -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check", "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg fix", "typecheck": "turbo typecheck", + "stripe:listen": "pnpm --filter '@kit/stripe' start", "supabase:start": "turbo dev --filter @kit/supabase-config", "supabase:stop": "pnpm --filter '@kit/supabase-config' stop", "supabase:reset": "pnpm --filter '@kit/supabase-config' reset", diff --git a/packages/billing-gateway/src/components/billing-portal-card.tsx b/packages/billing-gateway/src/components/billing-portal-card.tsx new file mode 100644 index 000000000..16126443c --- /dev/null +++ b/packages/billing-gateway/src/components/billing-portal-card.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { ArrowRightIcon } from '@radix-ui/react-icons'; + +import { Button } from '@kit/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; + +export function BillingPortalCard() { + return ( + + + Manage your Subscription + + + You can change your plan or cancel your subscription at any time. + + + + + + +

+ Visit the billing portal to manage your subscription (update payment + method, cancel subscription, etc.) +

+
+
+ ); +} diff --git a/packages/billing-gateway/src/components/billing-session-status.tsx b/packages/billing-gateway/src/components/billing-session-status.tsx index c553f6653..d37c7ffab 100644 --- a/packages/billing-gateway/src/components/billing-session-status.tsx +++ b/packages/billing-gateway/src/components/billing-session-status.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; -import { CheckIcon, ChevronRightIcon } from 'lucide-react'; +import { Check, ChevronRight } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -55,9 +55,9 @@ function SuccessSessionStatus({ 'flex flex-col items-center justify-center space-y-4 text-center' } > - @@ -87,7 +87,7 @@ function SuccessSessionStatus({ - + diff --git a/packages/billing-gateway/src/components/current-plan-card.tsx b/packages/billing-gateway/src/components/current-plan-card.tsx index fa5a3987e..4cfb05e9a 100644 --- a/packages/billing-gateway/src/components/current-plan-card.tsx +++ b/packages/billing-gateway/src/components/current-plan-card.tsx @@ -1 +1,68 @@ -export function CurrentPlanCard(props: React.PropsWithChildren<{}>) {} +import { formatDate } from 'date-fns'; +import { z } from 'zod'; + +import { BillingSchema, getProductPlanPairFromId } from '@kit/billing'; +import { Database } from '@kit/supabase/database'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; +import { If } from '@kit/ui/if'; + +export function CurrentPlanCard({ + subscription, + config, +}: React.PropsWithChildren<{ + subscription: Database['public']['Tables']['subscriptions']['Row']; + config: z.infer; +}>) { + const { plan, product } = getProductPlanPairFromId( + config, + subscription.variant_id, + ); + + return ( + + + {product.name} + + {product.description} + + + +
+
+ Your Current Plan: {plan.name} +
+
+ +
+
+ Your Subscription is currently {subscription.status} +
+
+ + +
+
+ Cancellation Date:{' '} + {formatDate(subscription.period_ends_at, 'P')} +
+
+
+ + +
+
+ Next Billing Date:{' '} + {formatDate(subscription.period_ends_at, 'P')}{' '} +
+
+
+
+
+ ); +} diff --git a/packages/billing-gateway/src/components/embedded-checkout.tsx b/packages/billing-gateway/src/components/embedded-checkout.tsx index 6c6c60a76..6b0a0c7ae 100644 --- a/packages/billing-gateway/src/components/embedded-checkout.tsx +++ b/packages/billing-gateway/src/components/embedded-checkout.tsx @@ -1,9 +1,14 @@ -import { lazy } from 'react'; +import { Suspense, forwardRef, lazy, memo, useMemo } from 'react'; import { Database } from '@kit/supabase/database'; +import { LoadingOverlay } from '@kit/ui/loading-overlay'; type BillingProvider = Database['public']['Enums']['billing_provider']; +const Fallback = ( + Loading Checkout... +); + export function EmbeddedCheckout( props: React.PropsWithChildren<{ checkoutToken: string; @@ -11,7 +16,10 @@ export function EmbeddedCheckout( onClose?: () => void; }>, ) { - const CheckoutComponent = loadCheckoutComponent(props.provider); + const CheckoutComponent = useMemo( + () => memo(loadCheckoutComponent(props.provider)), + [], + ); return ( { - return import('@kit/stripe/components').then((c) => ({ - default: c.StripeCheckout, - })); + return buildLazyComponent(() => { + return import('@kit/stripe/components').then(({ StripeCheckout }) => { + return { + default: StripeCheckout, + }; + }); }); } @@ -43,3 +53,33 @@ function loadCheckoutComponent(provider: BillingProvider) { throw new Error(`Unsupported provider: ${provider as string}`); } } + +function buildLazyComponent< + Cmp extends React.ComponentType< + React.PropsWithChildren<{ + onClose?: (() => unknown) | undefined; + checkoutToken: string; + }> + >, +>( + load: () => Promise<{ + default: Cmp; + }>, + fallback = Fallback, +) { + let LoadedComponent: ReturnType | null = null; + + const LazyComponent = forwardRef((props, ref) => { + if (!LoadedComponent) { + LoadedComponent = lazy(load); + } + + return ( + + + + ); + }); + + return memo(LazyComponent); +} diff --git a/packages/billing-gateway/src/components/index.ts b/packages/billing-gateway/src/components/index.ts index b32a7a6cd..e9f9e8b3b 100644 --- a/packages/billing-gateway/src/components/index.ts +++ b/packages/billing-gateway/src/components/index.ts @@ -2,3 +2,4 @@ export * from './plan-picker'; export * from './current-plan-card'; export * from './embedded-checkout'; export * from './billing-session-status'; +export * from './billing-portal-card'; diff --git a/packages/billing-gateway/src/components/plan-picker.tsx b/packages/billing-gateway/src/components/plan-picker.tsx index ad6b345d6..038055eff 100644 --- a/packages/billing-gateway/src/components/plan-picker.tsx +++ b/packages/billing-gateway/src/components/plan-picker.tsx @@ -1,7 +1,7 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { ArrowRightIcon } from 'lucide-react'; +import { ArrowRight } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -212,7 +212,7 @@ export function PlanPicker( ) : ( <> Proceed to payment - + )} diff --git a/packages/billing-gateway/src/services/billing-event-handler/billing-event-handler.service.ts b/packages/billing-gateway/src/services/billing-event-handler/billing-event-handler.service.ts index 4ce1d3c90..e3519609e 100644 --- a/packages/billing-gateway/src/services/billing-event-handler/billing-event-handler.service.ts +++ b/packages/billing-gateway/src/services/billing-event-handler/billing-event-handler.service.ts @@ -6,7 +6,7 @@ import { Database } from '@kit/supabase/database'; export class BillingEventHandlerService { constructor( - private readonly client: SupabaseClient, + private readonly clientProvider: () => SupabaseClient, private readonly strategy: BillingWebhookHandlerService, ) {} @@ -19,6 +19,8 @@ export class BillingEventHandlerService { return this.strategy.handleWebhookEvent(event, { onSubscriptionDeleted: async (subscriptionId: string) => { + const client = this.clientProvider(); + // Handle the subscription deleted event // here we delete the subscription from the database Logger.info( @@ -29,7 +31,7 @@ export class BillingEventHandlerService { 'Processing subscription deleted event', ); - const { error } = await this.client + const { error } = await client .from('subscriptions') .delete() .match({ id: subscriptionId }); @@ -47,6 +49,8 @@ export class BillingEventHandlerService { ); }, onSubscriptionUpdated: async (subscription) => { + const client = this.clientProvider(); + const ctx = { namespace: 'billing', subscriptionId: subscription.id, @@ -58,7 +62,7 @@ export class BillingEventHandlerService { // Handle the subscription updated event // here we update the subscription in the database - const { error } = await this.client + const { error } = await client .from('subscriptions') .update(subscription) .match({ id: subscription.id }); @@ -77,9 +81,11 @@ export class BillingEventHandlerService { Logger.info(ctx, 'Successfully updated subscription'); }, - onCheckoutSessionCompleted: async (subscription) => { + onCheckoutSessionCompleted: async (subscription, customerId) => { // Handle the checkout session completed event // here we add the subscription to the database + const client = this.clientProvider(); + const ctx = { namespace: 'billing', subscriptionId: subscription.id, @@ -89,12 +95,21 @@ export class BillingEventHandlerService { Logger.info(ctx, 'Processing checkout session completed event...'); - const { error } = await this.client.rpc('add_subscription', { - subscription, + const { id, ...data } = subscription; + + const { error } = await client.rpc('add_subscription', { + ...data, + subscription_id: subscription.id, + customer_id: customerId, + price_amount: subscription.price_amount ?? 0, + period_starts_at: subscription.period_starts_at!, + period_ends_at: subscription.period_ends_at!, + trial_starts_at: subscription.trial_starts_at!, + trial_ends_at: subscription.trial_ends_at!, }); if (error) { - Logger.error(ctx, 'Failed to add subscription'); + Logger.error({ ...ctx, error }, 'Failed to add subscription'); throw new Error('Failed to add subscription'); } diff --git a/packages/billing-gateway/src/services/billing-event-handler/billing-gateway-provider-factory.ts b/packages/billing-gateway/src/services/billing-event-handler/billing-gateway-provider-factory.ts index b6b42c926..917567930 100644 --- a/packages/billing-gateway/src/services/billing-event-handler/billing-gateway-provider-factory.ts +++ b/packages/billing-gateway/src/services/billing-event-handler/billing-gateway-provider-factory.ts @@ -10,11 +10,11 @@ import { BillingEventHandlerFactoryService } from './billing-gateway-factory.ser * defined in the host application. */ export async function getBillingEventHandlerService( - client: ReturnType, + clientProvider: () => ReturnType, provider: Database['public']['Enums']['billing_provider'], ) { const strategy = await BillingEventHandlerFactoryService.GetProviderStrategy(provider); - return new BillingEventHandlerService(client, strategy); + return new BillingEventHandlerService(clientProvider, strategy); } diff --git a/packages/billing/src/components/pricing-table.tsx b/packages/billing/src/components/pricing-table.tsx index af439a099..1845cf409 100644 --- a/packages/billing/src/components/pricing-table.tsx +++ b/packages/billing/src/components/pricing-table.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Link from 'next/link'; -import { CheckCircleIcon, SparklesIcon } from 'lucide-react'; +import { CheckCircle, Sparkles } from 'lucide-react'; import { z } from 'zod'; import { Button } from '@kit/ui/button'; @@ -147,7 +147,7 @@ function PricingItem( )} > - + {props.product.badge} @@ -238,7 +238,7 @@ function ListItem({ children }: React.PropsWithChildren) { return (
  • - +
    {children} @@ -275,7 +275,7 @@ function PlanIntervalSwitcher( > - + diff --git a/packages/billing/src/services/billing-webhook-handler.service.ts b/packages/billing/src/services/billing-webhook-handler.service.ts index 3df854464..b91f15a89 100644 --- a/packages/billing/src/services/billing-webhook-handler.service.ts +++ b/packages/billing/src/services/billing-webhook-handler.service.ts @@ -2,11 +2,6 @@ import { Database } from '@kit/supabase/database'; type SubscriptionObject = Database['public']['Tables']['subscriptions']; -type SubscriptionInsertParams = Omit< - SubscriptionObject['Insert'], - 'billing_customer_id' ->; - type SubscriptionUpdateParams = SubscriptionObject['Update']; /** @@ -19,7 +14,8 @@ export abstract class BillingWebhookHandlerService { event: unknown, params: { onCheckoutSessionCompleted: ( - subscription: SubscriptionInsertParams, + subscription: SubscriptionObject['Row'], + customerId: string, ) => Promise; onSubscriptionUpdated: ( diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index f528ff657..3bca1663e 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { CaretSortIcon, PersonIcon } from '@radix-ui/react-icons'; -import { CheckIcon, PlusIcon } from 'lucide-react'; +import { Check, Plus } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar'; import { Button } from '@kit/ui/button'; @@ -19,7 +19,7 @@ import { If } from '@kit/ui/if'; import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover'; import { cn } from '@kit/ui/utils'; -import { CreateOrganizationAccountDialog } from './create-organization-account-dialog'; +import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog'; interface AccountSelectorProps { accounts: Array<{ @@ -64,7 +64,7 @@ export function AccountSelector({ const Icon = (props: { item: string }) => { return ( - - + Create Organization @@ -208,7 +208,7 @@ export function AccountSelector({ - diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index 5683cba77..465ac7bb6 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -7,11 +7,11 @@ import Link from 'next/link'; import type { Session } from '@supabase/gotrue-js'; import { - EllipsisVerticalIcon, - HomeIcon, - LogOutIcon, - MessageCircleQuestionIcon, - ShieldIcon, + EllipsisVertical, + Home, + LogOut, + MessageCircleQuestion, + Shield, } from 'lucide-react'; import { @@ -87,7 +87,7 @@ export function PersonalAccountDropdown({
  • -
    diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx index 466b5105d..abb58c8ed 100644 --- a/packages/ui/src/makerkit/sidebar.tsx +++ b/packages/ui/src/makerkit/sidebar.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { cva } from 'class-variance-authority'; -import { ChevronDownIcon } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; import { z } from 'zod'; import { Button } from '../shadcn/button'; @@ -111,7 +111,7 @@ export function SidebarGroup({ {label} -