-
-
-
- )
+ return (
+
+
+
+ Billing Email
+
+ The email address for your billing account
+
+
+
+
+ (
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+ )
}
diff --git a/packages/web/src/app/[domain]/settings/billing/manageSubscriptionButton.tsx b/packages/web/src/app/[domain]/settings/billing/manageSubscriptionButton.tsx
index ec7a7b4b..9a6af18f 100644
--- a/packages/web/src/app/[domain]/settings/billing/manageSubscriptionButton.tsx
+++ b/packages/web/src/app/[domain]/settings/billing/manageSubscriptionButton.tsx
@@ -8,29 +8,27 @@ import { getCustomerPortalSessionLink } from "@/actions"
import { useDomain } from "@/hooks/useDomain";
import { OrgRole } from "@sourcebot/db";
import useCaptureEvent from "@/hooks/useCaptureEvent";
+import { ExternalLink, Loader2 } from "lucide-react";
+
export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole: OrgRole }) {
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const domain = useDomain();
const captureEvent = useCaptureEvent();
+
const redirectToCustomerPortal = async () => {
setIsLoading(true)
- try {
- const session = await getCustomerPortalSessionLink(domain)
- if (isServiceError(session)) {
- captureEvent('wa_manage_subscription_button_create_portal_session_fail', {
- error: session.errorCode,
- })
- } else {
- router.push(session)
- captureEvent('wa_manage_subscription_button_create_portal_session_success', {})
- }
- } catch (_error) {
+ const session = await getCustomerPortalSessionLink(domain);
+ if (isServiceError(session)) {
captureEvent('wa_manage_subscription_button_create_portal_session_fail', {
- error: "Unknown error",
- })
- } finally {
- setIsLoading(false)
+ error: session.errorCode,
+ });
+ setIsLoading(false);
+ } else {
+ captureEvent('wa_manage_subscription_button_create_portal_session_success', {})
+ router.push(session)
+ // @note: we don't want to set isLoading to false here since we want to show the loading
+ // spinner until the page is redirected.
}
}
@@ -42,7 +40,9 @@ export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole:
disabled={isLoading || !isOwner}
title={!isOwner ? "Only the owner of the org can manage the subscription" : undefined}
>
- {isLoading ? "Creating customer portal..." : "Manage Subscription"}
+ {isLoading && }
+ Manage Subscription
+
)
diff --git a/packages/web/src/app/[domain]/settings/billing/page.tsx b/packages/web/src/app/[domain]/settings/billing/page.tsx
index 53401cbd..dd35af44 100644
--- a/packages/web/src/app/[domain]/settings/billing/page.tsx
+++ b/packages/web/src/app/[domain]/settings/billing/page.tsx
@@ -1,12 +1,10 @@
import type { Metadata } from "next"
import { CalendarIcon, DollarSign, Users } from "lucide-react"
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ManageSubscriptionButton } from "./manageSubscriptionButton"
import { getSubscriptionData, getCurrentUserRole, getSubscriptionBillingEmail } from "@/actions"
import { isServiceError } from "@/lib/utils"
import { ChangeBillingEmailCard } from "./changeBillingEmailCard"
-import { CreditCard } from "lucide-react"
export const metadata: Metadata = {
title: "Billing | Settings",
@@ -50,11 +48,10 @@ export default async function BillingPage({
{/* Billing Email Card */}
-
+
-
Subscription Plan
diff --git a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx
index 65d99584..4b1c73c8 100644
--- a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx
+++ b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx
@@ -101,6 +101,7 @@ export const InviteMemberCard = ({ currentUserRole }: InviteMemberCardProps) =>
diff --git a/packages/web/src/app/onboard/components/orgCreateForm.tsx b/packages/web/src/app/onboard/components/orgCreateForm.tsx
index 47255be9..46d1da28 100644
--- a/packages/web/src/app/onboard/components/orgCreateForm.tsx
+++ b/packages/web/src/app/onboard/components/orgCreateForm.tsx
@@ -1,9 +1,9 @@
"use client"
-import { checkIfOrgDomainExists, createOrg } from "../../../actions"
+import { createOrg } from "../../../actions"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
-import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
@@ -15,6 +15,7 @@ import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card"
import { NEXT_PUBLIC_ROOT_DOMAIN } from "@/lib/environment.client";
import useCaptureEvent from "@/hooks/useCaptureEvent";
+import { orgNameSchema, orgDomainSchema } from "@/lib/schemas"
export function OrgCreateForm() {
@@ -24,24 +25,8 @@ export function OrgCreateForm() {
const [isLoading, setIsLoading] = useState(false);
const onboardingFormSchema = z.object({
- name: z.string()
- .min(2, { message: "Organization name must be at least 3 characters long." })
- .max(30, { message: "Organization name must be at most 30 characters long." }),
- domain: z.string()
- .min(2, { message: "Organization domain must be at least 3 characters long." })
- .max(20, { message: "Organization domain must be at most 20 characters long." })
- .regex(/^[a-z][a-z-]*[a-z]$/, {
- message: "Domain must start and end with a letter, and can only contain lowercase letters and dashes.",
- })
- .refine(async (domain) => {
- const doesDomainExist = await checkIfOrgDomainExists(domain);
- if (!isServiceError(doesDomainExist)) {
- captureEvent('wa_onboard_org_create_fail', {
- error: "Domain already exists",
- })
- }
- return isServiceError(doesDomainExist) || !doesDomainExist;
- }, "This domain is already taken."),
+ name: orgNameSchema,
+ domain: orgDomainSchema,
})
const form = useForm>({
@@ -80,13 +65,14 @@ export function OrgCreateForm() {
return (
-
+
(
-
+ Organization Name
+ {`Your organization's visible name within Sourcebot. For example, the name of your company or department.`}
(
-
- Organization Domain
+
+ Organization URL
+ {`Your organization's URL namespace. This is where your organization's Sourcebot instance will be accessible.`}
-
-
{NEXT_PUBLIC_ROOT_DOMAIN}/
-
+
+
{NEXT_PUBLIC_ROOT_DOMAIN}/
+
diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts
index 80f17662..735220d0 100644
--- a/packages/web/src/lib/posthogEvents.ts
+++ b/packages/web/src/lib/posthogEvents.ts
@@ -221,6 +221,16 @@ export type PosthogEventMap = {
//////////////////////////////////////////////////////////////////
wa_mobile_unsupported_splash_screen_dismissed: {},
wa_mobile_unsupported_splash_screen_displayed: {},
+ //////////////////////////////////////////////////////////////////
+ wa_org_name_updated_success: {},
+ wa_org_name_updated_fail: {
+ error: string,
+ },
+ //////////////////////////////////////////////////////////////////
+ wa_org_domain_updated_success: {},
+ wa_org_domain_updated_fail: {
+ error: string,
+ },
}
export type PosthogEvent = keyof PosthogEventMap;
\ No newline at end of file
diff --git a/packages/web/src/lib/schemas.ts b/packages/web/src/lib/schemas.ts
index 07b8e4cd..b61464ae 100644
--- a/packages/web/src/lib/schemas.ts
+++ b/packages/web/src/lib/schemas.ts
@@ -1,5 +1,7 @@
+import { checkIfOrgDomainExists } from "@/actions";
import { RepoIndexingStatus } from "@sourcebot/db";
import { z } from "zod";
+import { isServiceError } from "./utils";
export const searchRequestSchema = z.object({
query: z.string(),
maxMatchDisplayCount: z.number(),
@@ -188,3 +190,34 @@ export const verifyCredentialsResponseSchema = z.object({
email: z.string().optional(),
image: z.string().optional(),
});
+
+export const orgNameSchema = z.string().min(2, { message: "Organization name must be at least 3 characters long." });
+
+export const orgDomainSchema = z.string()
+ .min(2, { message: "Url must be at least 3 characters long." })
+ .max(50, { message: "Url must be at most 50 characters long." })
+ .regex(/^[a-z][a-z-]*[a-z]$/, {
+ message: "Url must start and end with a letter, and can only contain lowercase letters and dashes.",
+ })
+ .refine((domain) => {
+ const reserved = [
+ 'api',
+ 'login',
+ 'signup',
+ 'onboard',
+ 'redeem',
+ 'account',
+ 'settings',
+ 'staging',
+ 'support',
+ 'docs',
+ 'blog',
+ 'contact',
+ 'status'
+ ];
+ return !reserved.includes(domain);
+ }, "This url is reserved for internal use.")
+ .refine(async (domain) => {
+ const doesDomainExist = await checkIfOrgDomainExists(domain);
+ return isServiceError(doesDomainExist) || !doesDomainExist;
+ }, "This url is already taken.");
\ No newline at end of file