From b453c91c1bf2dc0248e16c45ff9ac6838dc599e1 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 24 Dec 2025 09:34:09 -0500 Subject: [PATCH 1/5] Add company search and signup flow improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /auth/signup endpoint with WorkOS screenHint: 'sign-up' for direct signup UX - Add organization search API endpoint to help users find their company - Add company type selection (Brand, Publisher, Agency, Ad Tech, Other) during onboarding - Add annual revenue tier selection during company onboarding - Improve onboarding card layout to prevent button overlap on search results - Allow users to request to join existing organizations - Show masked admin contact emails for privacy - Support signup flow redirects back to onboarding 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 --- .changeset/sixty-colts-bathe.md | 2 + server/public/admin-members.html | 74 ++- server/public/dashboard.html | 123 +++++ server/public/join-cta.js | 4 +- server/public/nav.js | 2 +- server/public/onboarding.html | 464 ++++++++++++++---- server/public/team.html | 133 +++++ .../027_member_type_and_revenue.sql | 24 + ...king_groups.sql => 028_working_groups.sql} | 0 ...ers_only.sql => 029_post_members_only.sql} | 0 server/src/db/organization-db.ts | 79 ++- server/src/http.ts | 316 +++++++++++- server/tests/unit/database-schema.test.ts | 3 + tests/billing/organization-db.test.ts | 2 +- 14 files changed, 1105 insertions(+), 121 deletions(-) create mode 100644 .changeset/sixty-colts-bathe.md create mode 100644 server/src/db/migrations/027_member_type_and_revenue.sql rename server/src/db/migrations/{024_working_groups.sql => 028_working_groups.sql} (100%) rename server/src/db/migrations/{025_post_members_only.sql => 029_post_members_only.sql} (100%) diff --git a/.changeset/sixty-colts-bathe.md b/.changeset/sixty-colts-bathe.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/sixty-colts-bathe.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/public/admin-members.html b/server/public/admin-members.html index e5a7a046eb..d615dee74f 100644 --- a/server/public/admin-members.html +++ b/server/public/admin-members.html @@ -368,16 +368,17 @@

Member Management

Company + Type + Revenue Owner Email Subscription Amount Renewal Date - Agreement Actions - No members found + No members found @@ -511,7 +512,7 @@

This action cannot be undone

countDiv.textContent = `Showing ${filteredMembers.length} of ${allMembers.length} members`; if (filteredMembers.length === 0) { - tbody.innerHTML = 'No members found'; + tbody.innerHTML = 'No members found'; return; } @@ -519,6 +520,31 @@

This action cannot be undone

const statusClass = `status-${member.subscription_status || 'none'}`.replace('_', ''); const statusText = member.subscription_status || 'none'; + // Format company type + const companyTypeLabels = { + brand: 'Brand', + publisher: 'Publisher', + agency: 'Agency', + adtech: 'Ad Tech', + other: 'Other' + }; + const companyTypeText = member.is_personal + ? 'Individual' + : (companyTypeLabels[member.company_type] || '-'); + + // Format revenue tier + const revenueTierLabels = { + under_1m: '< $1M', + '1m_5m': '$1-5M', + '5m_50m': '$5-50M', + '50m_250m': '$50-250M', + '250m_1b': '$250M-1B', + '1b_plus': '$1B+' + }; + const revenueTierText = member.is_personal + ? '-' + : (revenueTierLabels[member.revenue_tier] || '-'); + // Format subscription amount let amountText = '-'; if (member.subscription_amount) { @@ -543,10 +569,6 @@

This action cannot be undone

} } - const agreementText = member.agreement_signed_at - ? new Date(member.agreement_signed_at).toLocaleDateString() - : 'Not signed'; - // Stripe customer link const stripeLink = member.stripe_customer_id ? `https://dashboard.stripe.com/test/customers/${member.stripe_customer_id}` @@ -555,11 +577,12 @@

This action cannot be undone

return ` ${member.company_name || 'Unknown'} + ${companyTypeText} + ${revenueTierText} ${member.owner_email || 'No email'} ${statusText} ${amountText} ${periodEndText} - ${agreementText} ${stripeLink ? `💳` : ''} @@ -577,22 +600,47 @@

This action cannot be undone

} function exportToCSV() { - const headers = ['Company Name', 'Owner Email', 'Subscription Status', 'Current Period End', 'Agreement Signed', 'Created']; + const headers = ['Company Name', 'Company Type', 'Revenue Tier', 'Owner Email', 'Subscription Status', 'Amount', 'Current Period End', 'Created']; const rows = filteredMembers.map(member => { const periodEnd = member.subscription_current_period_end ? new Date(member.subscription_current_period_end * 1000).toLocaleDateString() : ''; - const agreementSigned = member.agreement_signed_at - ? new Date(member.agreement_signed_at).toLocaleDateString() - : ''; const created = new Date(member.created_at).toLocaleDateString(); + // Format company type + const companyTypeLabels = { + brand: 'Brand', + publisher: 'Publisher', + agency: 'Agency', + adtech: 'Ad Tech', + other: 'Other' + }; + const companyType = member.is_personal ? 'Individual' : (companyTypeLabels[member.company_type] || ''); + + // Format revenue tier + const revenueTierLabels = { + under_1m: '< $1M', + '1m_5m': '$1-5M', + '5m_50m': '$5-50M', + '50m_250m': '$50-250M', + '250m_1b': '$250M-1B', + '1b_plus': '$1B+' + }; + const revenueTier = member.is_personal ? '' : (revenueTierLabels[member.revenue_tier] || ''); + + // Format amount + const amount = member.subscription_amount + ? `$${(member.subscription_amount / 100).toFixed(2)}/${member.subscription_interval || ''}` + : ''; + return [ member.company_name, + companyType, + revenueTier, member.owner_email, member.subscription_status || 'none', + amount, periodEnd, - agreementSigned, created ]; }); diff --git a/server/public/dashboard.html b/server/public/dashboard.html index 7d2ee999ab..f0ed55f9a0 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -525,6 +525,16 @@

+ + + @@ -268,7 +268,7 @@ function renderJoinCta(options = {}) { diff --git a/server/public/nav.js b/server/public/nav.js index 4e72f29a5a..f38fddfcae 100644 --- a/server/public/nav.js +++ b/server/public/nav.js @@ -99,7 +99,7 @@ // Not logged in - show login/signup (links to AAO) authSection = ` Log in - Sign up + Sign up `; } } diff --git a/server/public/onboarding.html b/server/public/onboarding.html index 39b86a2fe3..282ca69dc9 100644 --- a/server/public/onboarding.html +++ b/server/public/onboarding.html @@ -422,6 +422,16 @@ border-radius: var(--radius-md); text-align: center; } + + /* Radio option hover styles */ + .radio-option:hover { + border-color: var(--color-brand); + background: var(--color-primary-50); + } + .radio-option:has(input:checked) { + border-color: var(--color-brand); + background: var(--color-primary-100); + } @@ -440,61 +450,110 @@

You have pending invitations

- -
-
+ +
+

Are you joining as an individual or on behalf of a company?

+ +
-
👥
-
My company is already a member
+
👤
+
I'm joining as an individual
- Join your company's existing membership. You may need an admin to approve your access. + Perfect for consultants, freelancers, academics, or those exploring the space.
-
+
🏢
-
Register my company as a new member
+
I'm joining on behalf of a company
- Start a new company membership. You'll be able to invite team members after setup. + For brands, publishers, agencies, and ad tech providers.
+
-
-
-
👤
-
Join as an individual professional
+ +
+ ← Back +

Company Membership

+

Is your company already a member?

+ +
+
+
+
👥
+
Yes, my company is already a member
+
+
+ Join your company's existing membership. You may need an admin to approve your access. +
-
- Join with an individual membership. Perfect for consultants, freelancers, or those exploring the space. + +
+
+
+
No, register my company as a new member
+
+
+ Start a new company membership. You'll be able to invite team members after setup. +
- ← Back to options -

Join Your Company's Membership

+ ← Back +

Find Your Company

+

+ Search for your company or check if we found a match based on your email. +

+ + +
+ +
+ + +
+
+ + + + + + -