Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/(auth)/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { SignIn } from '@clerk/nextjs';
import { redirect } from 'next/navigation';

import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import {
DEFAULT_CLERK_AFTER_AUTH_URL,
DEFAULT_CLERK_SIGN_IN_URL,
DEFAULT_CLERK_SIGN_UP_URL,
hasRequiredValidClerkEnv,
} from '@/lib/clerk-config';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { resolveSignedInAppDestination } from '@/lib/public-auth-routing';

export default async function SignInPage() {
Expand Down Expand Up @@ -39,7 +39,7 @@ export default async function SignInPage() {
if (userId) {
const [adminSession, business] = await Promise.all([
getAdminSession(),
getBusinessForOwnerClerkId(userId),
getOwnedBusinessForClerkUser(await currentUser()),
]);

redirect(
Expand Down
6 changes: 3 additions & 3 deletions app/(auth)/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { SignUp } from '@clerk/nextjs';
import { redirect } from 'next/navigation';

import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import {
DEFAULT_CLERK_AFTER_AUTH_URL,
DEFAULT_CLERK_SIGN_IN_URL,
DEFAULT_CLERK_SIGN_UP_URL,
hasRequiredValidClerkEnv,
} from '@/lib/clerk-config';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { resolveSignedInAppDestination } from '@/lib/public-auth-routing';

function getIntentCopy(intent: string | undefined) {
Expand Down Expand Up @@ -65,7 +65,7 @@ export default async function SignUpPage({
if (userId) {
const [adminSession, business] = await Promise.all([
getAdminSession(),
getBusinessForOwnerClerkId(userId),
getOwnedBusinessForClerkUser(await currentUser()),
]);

redirect(
Expand Down
8 changes: 4 additions & 4 deletions app/admin/[businessId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,12 @@ export default async function AdminBusinessDetailPage({
: ownerAction === 'invited'
? {
variant: 'success' as const,
message: `Owner invitation sent to ${ownerEmail}. The owner step stays pending until they accept, then use Connect existing owner to finish linking.`,
message: `Owner invitation sent to ${ownerEmail}. CallbackCloser will auto-connect the owner after they accept and sign in, or show a one-click connect action here if review is still needed.`,
}
: ownerAction === 'resent'
? {
variant: 'success' as const,
message: `Owner invitation resent to ${ownerEmail}. The owner step stays pending until they accept, then use Connect existing owner to finish linking.`,
message: `Owner invitation resent to ${ownerEmail}. CallbackCloser will auto-connect the owner after they accept and sign in, or show a one-click connect action here if review is still needed.`,
}
: provisioned
? {
Expand Down Expand Up @@ -1122,9 +1122,9 @@ export default async function AdminBusinessDetailPage({
{ownerAction === 'connected'
? `Existing owner connected for ${ownerEmail}.`
: ownerAction === 'invited'
? `Owner invitation sent to ${ownerEmail}.`
? `Owner invitation sent to ${ownerEmail}. CallbackCloser will auto-connect after acceptance when it is safe to do so.`
: ownerAction === 'resent'
? `Owner invitation resent to ${ownerEmail}.`
? `Owner invitation resent to ${ownerEmail}. CallbackCloser will auto-connect after acceptance when it is safe to do so.`
: 'Owner state updated.'}
</div>
) : null}
Expand Down
5 changes: 3 additions & 2 deletions app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

import { logAuditEvent } from '@/lib/audit-log';
import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { getCorrelationIdFromRequest, reportApplicationError, withCorrelationIdHeader } from '@/lib/observability';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { isAllowedRequestOrigin } from '@/lib/request-origin';
import { getStripe } from '@/lib/stripe';
import { absoluteUrl } from '@/lib/url';
Expand All @@ -29,7 +30,7 @@ export async function POST(request: Request) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/sign-in'), { status: 303 }));
}

const business = await db.business.findUnique({ where: { ownerClerkId: userId } });
const business = await getOwnedBusinessForClerkUser(await currentUser());
if (!business) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/app'), { status: 303 }));
}
Expand Down
6 changes: 3 additions & 3 deletions app/api/stripe/portal/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

import { logAuditEvent } from '@/lib/audit-log';
import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { getCorrelationIdFromRequest, reportApplicationError, withCorrelationIdHeader } from '@/lib/observability';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { isAllowedRequestOrigin } from '@/lib/request-origin';
import { getStripe } from '@/lib/stripe';
import { absoluteUrl } from '@/lib/url';
Expand All @@ -24,7 +24,7 @@ export async function POST(request: Request) {
return withCorrelation(NextResponse.redirect(absoluteUrl('/sign-in'), { status: 303 }));
}

const business = await db.business.findUnique({ where: { ownerClerkId: userId } });
const business = await getOwnedBusinessForClerkUser(await currentUser());
if (!business?.stripeCustomerId) {
return withCorrelation(
NextResponse.redirect(absoluteUrl('/app/billing?error=No%20Stripe%20customer%20for%20this%20business'), { status: 303 })
Expand Down
15 changes: 3 additions & 12 deletions app/api/twilio/provision-number/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { getCorrelationIdFromRequest, withCorrelationIdHeader } from '@/lib/observability';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { isAllowedRequestOrigin } from '@/lib/request-origin';
import { logTwilioError, logTwilioWarn } from '@/lib/twilio-logging';
import { getTwilioProvisioningBlockReason, provisionPhoneNumber } from '@/lib/twilio-provision';
Expand Down Expand Up @@ -46,16 +46,7 @@ export async function POST(request: Request) {
return withCorrelation(NextResponse.json({ error: 'Unauthorized' }, { status: 401 }));
}

const business = await db.business.findUnique({
where: { ownerClerkId: userId },
select: {
id: true,
name: true,
twilioSubaccountSid: true,
twilioPhoneNumber: true,
twilioPhoneNumberSid: true,
},
});
const business = await getOwnedBusinessForClerkUser(await currentUser());

if (!business) {
return withCorrelation(NextResponse.json({ error: 'Business not found' }, { status: 404 }));
Expand Down
22 changes: 7 additions & 15 deletions app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Badge } from '@/components/ui/badge';
import { getAdminSession } from '@/lib/admin';
import { getAdminCustomerActingContext } from '@/lib/admin-customer-context';
import { buildAdminCustomerExitHref } from '@/lib/admin-customer-paths';
import { ensurePendingBusinessForOwner } from '@/lib/customer-setup-handoff';
import { getCustomerWorkspaceNotice, shouldShowCustomerSetupWaitingPage } from '@/lib/customer-setup';
import { db } from '@/lib/db';
import { getOwnedBusinessForClerkUser, getOrCreateOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { getPortfolioDemoBusiness, isPortfolioDemoMode } from '@/lib/portfolio-demo';
import { getCustomerSystemStatus } from '@/lib/system-status';

Expand Down Expand Up @@ -55,30 +55,22 @@ export default async function AppLayout({ children }: { children: React.ReactNod
}

const adminSession = await getAdminSession();
const signedInUser = adminCustomerContext ? null : await currentUser();
if (!adminCustomerContext && !signedInUser) {
redirect('/sign-in');
}

const existingBusiness = adminCustomerContext
? adminCustomerContext.business
: await db.business.findUnique({ where: { ownerClerkId: userId } });
: await getOwnedBusinessForClerkUser(signedInUser);
if (!adminCustomerContext && adminSession?.isAdmin && !existingBusiness) {
redirect('/admin?intent=new-business-pilot');
}
const business =
existingBusiness ||
(adminCustomerContext
? null
: await (async () => {
const user = await currentUser();
const ownerEmail =
(user?.primaryEmailAddressId
? user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)?.emailAddress
: user?.emailAddresses[0]?.emailAddress) || null;

return ensurePendingBusinessForOwner(userId, {
businessName: typeof user?.publicMetadata?.businessName === 'string' ? user.publicMetadata.businessName : null,
ownerEmail,
ownerName: user?.fullName || [user?.firstName, user?.lastName].filter(Boolean).join(' ') || null,
});
})());
: await getOrCreateOwnedBusinessForClerkUser(signedInUser));
const successfulLeadCount = business
? await db.lead.count({ where: { businessId: business.id, OR: [{ ownerNotifiedAt: { not: null } }, { notifiedAt: { not: null } }] } })
: 0;
Expand Down
2 changes: 2 additions & 0 deletions app/app/onboarding/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { revalidatePath } from 'next/cache';

import { deriveTwilioNumberSetupModeFromPhoneSetupPath } from '@/lib/business-phone-setup';
import { upsertBusinessForOwner } from '@/lib/business';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { onboardingSchema } from '@/lib/validators';

const DEFAULT_POST_ONBOARDING_REDIRECT = '/app/settings';
Expand Down Expand Up @@ -38,6 +39,7 @@ export async function saveOnboardingAction(formData: FormData) {
}

const user = await currentUser();
await getOwnedBusinessForClerkUser(user);
const ownerEmail =
(user?.primaryEmailAddressId
? user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)?.emailAddress
Expand Down
6 changes: 3 additions & 3 deletions app/buy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

import { db } from '@/lib/db';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';

type PlanParam = 'starter' | 'pro';

Expand Down Expand Up @@ -32,7 +32,7 @@ export default async function BuyPage({ searchParams }: { searchParams?: Record<
redirect(`/sign-up?redirect_url=${encodeURIComponent(buyPath)}`);
}

const business = await db.business.findUnique({ where: { ownerClerkId: userId } });
const business = await getOwnedBusinessForClerkUser(await currentUser());
if (!business) {
redirect('/app');
}
Expand Down
6 changes: 3 additions & 3 deletions app/start-free-pilot/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { auth } from '@clerk/nextjs/server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import { getOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { resolvePublicPilotDestination } from '@/lib/public-auth-routing';

export default async function StartFreePilotPage() {
Expand All @@ -14,7 +14,7 @@ export default async function StartFreePilotPage() {

const [adminSession, business] = await Promise.all([
getAdminSession(),
getBusinessForOwnerClerkId(userId),
getOwnedBusinessForClerkUser(await currentUser()),
]);

redirect(
Expand Down
4 changes: 2 additions & 2 deletions lib/admin-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export function buildAdminNextStep(params: {
title: business.ownerInviteSentAt ? 'Owner invitation is still pending' : 'Owner account still needs setup',
detail: ownerEmail
? business.ownerInviteSentAt
? 'The invite has been sent, but the owner account is not attached yet. Wait for acceptance or use Connect existing owner after they create the account.'
? 'The invite has been sent, but the owner account is not attached yet. CallbackCloser should auto-connect the owner after acceptance, or this panel will expose Connect existing owner as the fallback.'
: 'The business is saved, but the owner account still needs a deliberate admin action. Use Invite owner by email or Connect existing owner.'
: 'Add the owner email first, then choose Invite owner by email or Connect existing owner.',
tone: 'attention',
Expand Down Expand Up @@ -590,7 +590,7 @@ export function buildAdminOnboardingConfidence(params: {
? 'A CallbackCloser owner account is linked to this business.'
: ownerEmail
? business.ownerInviteSentAt
? 'The owner invite is still pending or the accepted account still needs linking.'
? 'The owner invite is still pending, or CallbackCloser still needs a safe owner-link confirmation.'
: 'Choose Invite owner by email or Connect existing owner.'
: 'Add the owner email and then choose the correct owner setup action.',
},
Expand Down
6 changes: 3 additions & 3 deletions lib/admin-provisioning-presenters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export function deriveAdminOwnerState(input: {
return {
status: 'accepted_needs_connection',
statusLabel: 'Owner account ready to connect',
detail: 'The owner now appears to have a CallbackCloser account. Finish by using Connect existing owner.',
detail: 'The owner now appears to have a verified CallbackCloser account. Use Connect existing owner only if CallbackCloser could not safely auto-link it.',
badgeVariant: 'secondary',
connected: false,
pending: true,
Expand Down Expand Up @@ -270,7 +270,7 @@ export function deriveAdminOwnerState(input: {

return {
status: 'missing',
statusLabel: 'No owner connected',
statusLabel: 'No owner invited',
detail: 'Add the owner email, then either invite them or connect their existing CallbackCloser account.',
badgeVariant: 'destructive',
connected: false,
Expand Down Expand Up @@ -332,7 +332,7 @@ export function buildAdminProvisioningChecklist({
detail: ownerConnected
? 'A Clerk user is linked to this business.'
: ownerEmail
? 'Owner setup is still incomplete. Send the invite or connect the existing CallbackCloser account.'
? 'Owner setup is still incomplete. Send the invite or connect the existing CallbackCloser account, and CallbackCloser will auto-link verified invite acceptances when it is safe.'
: 'Add an owner email, then choose Invite owner by email or Connect existing owner.',
},
{
Expand Down
12 changes: 2 additions & 10 deletions lib/admin-provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ensureBusinessNotificationSettings } from '@/lib/business-notification-settings';
import { db } from '@/lib/db';
import { getConfiguredAppBaseUrl } from '@/lib/env.server';
import { findVerifiedClerkUserByEmail } from '@/lib/owner-linking';
import {
attachNumberToMessagingService,
createMessagingServiceForBusiness,
Expand All @@ -37,16 +38,7 @@ export {
export type { AdminProvisioningChecklistItem } from '@/lib/admin-provisioning-presenters';

export async function findClerkUserByEmail(email: string) {
const normalized = email.trim().toLowerCase();
if (!normalized) return null;

const client = await clerkClient();
const result = await client.users.getUserList({
emailAddress: [normalized],
limit: 1,
});

return result.data[0] ?? null;
return findVerifiedClerkUserByEmail(email);
}

function invitationBelongsToBusiness(invitation: { publicMetadata: Record<string, unknown> | null }, businessId: string) {
Expand Down
2 changes: 1 addition & 1 deletion lib/admin-setup-remediation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function buildAdminSetupPanels(params: {
'Confirm the owner email you want attached to this business.',
'If the owner already has a CallbackCloser login, use Connect existing owner.',
'If not, send an invite from this panel.',
'Come back to this step after the owner appears as connected.',
'After the owner accepts and signs in, CallbackCloser should connect the account automatically or show Connect existing owner here as the fallback.',
],
verification: [
'The step should show Owner connected.',
Expand Down
28 changes: 11 additions & 17 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { redirect } from 'next/navigation';

import { getAdminCustomerActingContext } from '@/lib/admin-customer-context';
import { getAdminSession } from '@/lib/admin';
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
import { ensurePendingBusinessForOwner } from '@/lib/customer-setup-handoff';
import { getOwnedBusinessForClerkUser, getOrCreateOwnedBusinessForClerkUser } from '@/lib/owner-linking';
import { getPortfolioDemoAuth, getPortfolioDemoBusiness, isPortfolioDemoMode } from '@/lib/portfolio-demo';

export async function requireAuth() {
Expand Down Expand Up @@ -32,7 +31,10 @@ export async function getCurrentBusiness() {
const { userId } = await auth();
if (!userId) return null;

return getBusinessForOwnerClerkId(userId);
const user = await currentUser();
if (!user) return null;

return getOwnedBusinessForClerkUser(user);
}

export async function requireBusiness() {
Expand All @@ -45,25 +47,17 @@ export async function requireBusiness() {
return adminCustomerContext.business;
}

const { userId } = await requireAuth();
await requireAuth();
const adminSession = await getAdminSession();
if (adminSession?.isAdmin) {
redirect('/admin?intent=new-business-pilot');
}

let business = await getBusinessForOwnerClerkId(userId);
if (!business) {
const user = await currentUser();
const ownerEmail =
(user?.primaryEmailAddressId
? user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)?.emailAddress
: user?.emailAddresses[0]?.emailAddress) || null;

business = await ensurePendingBusinessForOwner(userId, {
businessName: typeof user?.publicMetadata?.businessName === 'string' ? user.publicMetadata.businessName : null,
ownerEmail,
ownerName: user?.fullName || [user?.firstName, user?.lastName].filter(Boolean).join(' ') || null,
});
const user = await currentUser();
if (!user) {
redirect('/sign-in');
}

const business = await getOrCreateOwnedBusinessForClerkUser(user);
return business;
}
Loading
Loading