tenseleyflow/shithub / 5f78996

Browse files

Add organization plan onboarding route

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5f78996f71e66b7c300038df98d972977056ed77
Parents
72a8056
Tree
ff15ae1

11 changed files

StatusFile+-
M docs/internal/billing.md 3 3
M docs/internal/orgs.md 6 3
M docs/internal/runbooks/stripe-billing.md 1 1
M internal/web/handlers/orgs/create_test.go 51 1
M internal/web/handlers/orgs/orgs.go 22 4
M internal/web/static/css/shithub.css 132 9
M internal/web/templates/_nav.html 1 1
M internal/web/templates/explore/index.html 1 1
M internal/web/templates/orgs/new.html 23 1
M internal/web/templates/orgs/new_plan.html 73 73
M internal/web/templates/settings/organizations.html 3 3
docs/internal/billing.mdmodified
@@ -29,9 +29,9 @@ Initial decisions:
2929
 - Stripe Billing is the first payment processor.
3030
 - PayPal, manual invoices, SAML, SCIM, LDAP, enterprise account
3131
   hierarchy, and contracts are deferred.
32
-- Self-serve organization creation should present plan selection first
33
-  when billing is enabled; choosing Team creates the organization and
34
-  then hands the owner into billing to finish checkout.
32
+- Self-serve organization creation presents `/organizations/plan` as
33
+  the canonical plan selector. Choosing Team creates the organization
34
+  and then hands the owner into billing to finish checkout.
3535
 
3636
 The fairness rule is explicit: public/open-source collaboration should
3737
 stay generous. Paid gates focus on private collaboration, hosted cost,
docs/internal/orgs.mdmodified
@@ -20,7 +20,8 @@ already present from 0017 with the XOR CHECK).
2020
 ## Routing
2121
 
2222
 ```
23
-GET  /organizations/new            plan picker / create form (auth required)
23
+GET  /organizations/plan           plan picker (auth required)
24
+GET  /organizations/new            create form (auth required)
2425
 POST /organizations                create submit
2526
 GET  /{slug}                       /{user-or-org} — dispatched via principals.Resolve
2627
 POST /{slug}/pins                  owner-only org profile pin customization
@@ -195,8 +196,10 @@ the human-facing summary, but product behavior goes through
195196
 `org_billing_states`; production handlers must not branch directly on
196197
 `orgs.plan` for paid feature access.
197198
 
198
-When billing is fully configured, `/organizations/new` starts with a
199
-Free / Team / Enterprise plan picker. Choosing Team creates the
199
+`/organizations/plan` is the canonical Free / Team / Enterprise plan
200
+picker. Existing "New organization" links route there. When Stripe
201
+Billing is not fully configured, Team remains visible but disabled;
202
+site admins see operator setup copy. Choosing Team creates the
200203
 organization first and then redirects the owner to
201204
 `/organizations/{org}/settings/billing` to begin Stripe Checkout.
202205
 Existing owner-managed orgs also link to that billing page from
docs/internal/runbooks/stripe-billing.mdmodified
@@ -77,7 +77,7 @@ worker uses the same Stripe credentials for seat quantity sync.
7777
 ## Smoke test
7878
 
7979
 1. Sign in as an owner.
80
-2. Visit `/organizations/new`.
80
+2. Visit `/organizations/plan`.
8181
 3. Confirm the plan picker appears.
8282
 4. Choose Team, create a test organization, and confirm redirect to
8383
    `/organizations/<org>/settings/billing?notice=team-created`.
internal/web/handlers/orgs/create_test.gomodified
@@ -52,6 +52,32 @@ func TestOrgNewFormShowsPlanSelectionWhenBillingEnabled(t *testing.T) {
5252
 	}
5353
 }
5454
 
55
+func TestOrgPlanSelectionRendersWhenBillingDisabled(t *testing.T) {
56
+	t.Parallel()
57
+	srv, _ := newOrgCreateServer(t, false)
58
+	t.Cleanup(srv.Close)
59
+
60
+	resp, err := srv.Client().Get(srv.URL + "/organizations/plan")
61
+	if err != nil {
62
+		t.Fatalf("GET organizations/plan: %v", err)
63
+	}
64
+	body, _ := io.ReadAll(resp.Body)
65
+	_ = resp.Body.Close()
66
+	if resp.StatusCode != http.StatusOK {
67
+		t.Fatalf("status=%d body=%s", resp.StatusCode, body)
68
+	}
69
+	for _, want := range []string{
70
+		"PLAN_PAGE",
71
+		"CONFIGURED=false",
72
+		"/organizations/new?plan=free",
73
+		"/organizations/new?plan=team",
74
+	} {
75
+		if !strings.Contains(string(body), want) {
76
+			t.Fatalf("missing %q in body: %s", want, body)
77
+		}
78
+	}
79
+}
80
+
5581
 func TestOrgNewFormRendersSetupForSelectedTeamPlan(t *testing.T) {
5682
 	t.Parallel()
5783
 	srv, _ := newOrgCreateServer(t, true)
@@ -90,6 +116,29 @@ func TestOrgNewFormSkipsPlanSelectionWhenBillingDisabled(t *testing.T) {
90116
 	}
91117
 }
92118
 
119
+func TestOrgCreateRequiresTermsAcceptance(t *testing.T) {
120
+	t.Parallel()
121
+	srv, _ := newOrgCreateServer(t, false)
122
+	t.Cleanup(srv.Close)
123
+
124
+	resp, err := srv.Client().PostForm(srv.URL+"/organizations", url.Values{
125
+		"plan":         {"free"},
126
+		"slug":         {"acme"},
127
+		"display_name": {"Acme"},
128
+	})
129
+	if err != nil {
130
+		t.Fatalf("POST organizations: %v", err)
131
+	}
132
+	body, _ := io.ReadAll(resp.Body)
133
+	_ = resp.Body.Close()
134
+	if resp.StatusCode != http.StatusOK {
135
+		t.Fatalf("status=%d body=%s", resp.StatusCode, body)
136
+	}
137
+	if !strings.Contains(string(body), "ERROR=You must accept the terms") {
138
+		t.Fatalf("expected terms error, got: %s", body)
139
+	}
140
+}
141
+
93142
 func TestOrgCreateTeamPlanRedirectsToBillingSettings(t *testing.T) {
94143
 	t.Parallel()
95144
 	srv, pool := newOrgCreateServer(t, true)
@@ -103,6 +152,7 @@ func TestOrgCreateTeamPlanRedirectsToBillingSettings(t *testing.T) {
103152
 		"slug":          {"acme"},
104153
 		"display_name":  {"Acme"},
105154
 		"billing_email": {"billing@example.com"},
155
+		"accept_terms":  {"1"},
106156
 	})
107157
 	if err != nil {
108158
 		t.Fatalf("POST organizations: %v", err)
@@ -131,7 +181,7 @@ func newOrgCreateServer(t *testing.T, billingEnabled bool) (*httptest.Server, *p
131181
 
132182
 	tmplFS := fstest.MapFS{
133183
 		"_layout.html":       {Data: []byte(`{{ define "layout" }}<html><body>{{ template "page" . }}</body></html>{{ end }}`)},
134
-		"orgs/new_plan.html": {Data: []byte(`{{ define "page" }}PLAN_PAGE{{ with .Error }};ERROR={{ . }}{{ end }};FREE=/organizations/new?plan=free;TEAM=/organizations/new?plan=team;ENTERPRISE=/organizations/new?plan=enterprise{{ end }}`)},
184
+		"orgs/new_plan.html": {Data: []byte(`{{ define "page" }}PLAN_PAGE;CONFIGURED={{ .BillingConfigured }}{{ with .Error }};ERROR={{ . }}{{ end }};FREE=/organizations/new?plan=free;TEAM=/organizations/new?plan=team;ENTERPRISE=/organizations/new?plan=enterprise{{ end }}`)},
135185
 		"orgs/new.html":      {Data: []byte(`{{ define "page" }}FORM_PLAN={{ .Form.SelectedTier }};ACTION=/organizations{{ with .Error }};ERROR={{ . }}{{ end }}{{ end }}`)},
136186
 		"errors/403.html":    {Data: []byte(`{{ define "page" }}403{{ end }}`)},
137187
 		"errors/404.html":    {Data: []byte(`{{ define "page" }}404{{ end }}`)},
internal/web/handlers/orgs/orgs.gomodified
@@ -2,7 +2,8 @@
22
 
33
 // Package orgs wires the S30 organization web surface:
44
 //
5
-//	GET  /organizations/new            plan selection / create form
5
+//	GET  /organizations/plan           plan selection
6
+//	GET  /organizations/new            create form
67
 //	POST /organizations                create submit
78
 //	GET  /orgs/{org}/repositories                          repository list
89
 //	GET  /{org}/people                                      members + pending invites + invite form
@@ -87,12 +88,13 @@ func New(d Deps) (*Handlers, error) {
8788
 	return &Handlers{d: d}, nil
8889
 }
8990
 
90
-// MountCreate registers /organizations/new, POST /organizations, and
91
+// MountCreate registers /organizations/plan, /organizations/new, POST /organizations, and
9192
 // organization settings routes under /organizations/{org}/settings/*.
9293
 // Caller wraps these in RequireUser since they require a logged-in
9394
 // actor. The /organizations prefix is on the auth-reserved list so it
9495
 // never shadows a user/org slug.
9596
 func (h *Handlers) MountCreate(r chi.Router) {
97
+	r.Get("/organizations/plan", h.planSelection)
9698
 	r.Get("/organizations/new", h.newForm)
9799
 	r.Post("/organizations", h.createSubmit)
98100
 	r.Get("/organizations/{org}/settings/profile", h.settingsProfile)
@@ -183,6 +185,15 @@ func parseUserIDParam(s string) (int64, error) {
183185
 
184186
 // ─── create ────────────────────────────────────────────────────────
185187
 
188
+func (h *Handlers) planSelection(w http.ResponseWriter, r *http.Request) {
189
+	viewer := middleware.CurrentUserFromContext(r.Context())
190
+	if viewer.IsAnonymous() {
191
+		http.Redirect(w, r, "/login?next=/organizations/plan", http.StatusSeeOther)
192
+		return
193
+	}
194
+	h.renderPlanSelection(w, r, "")
195
+}
196
+
186197
 func (h *Handlers) newForm(w http.ResponseWriter, r *http.Request) {
187198
 	viewer := middleware.CurrentUserFromContext(r.Context())
188199
 	if viewer.IsAnonymous() {
@@ -209,6 +220,7 @@ type orgCreateForm struct {
209220
 	BillingEmail string
210221
 	GitHubOrg    string
211222
 	GitHubToken  string
223
+	AcceptTerms  bool
212224
 }
213225
 
214226
 func (h *Handlers) createSubmit(w http.ResponseWriter, r *http.Request) {
@@ -228,11 +240,16 @@ func (h *Handlers) createSubmit(w http.ResponseWriter, r *http.Request) {
228240
 		BillingEmail: strings.TrimSpace(r.PostFormValue("billing_email")),
229241
 		GitHubOrg:    strings.TrimSpace(r.PostFormValue("github_org")),
230242
 		GitHubToken:  strings.TrimSpace(r.PostFormValue("github_token")),
243
+		AcceptTerms:  r.PostFormValue("accept_terms") != "",
231244
 	}
232245
 	if form.SelectedTier == orgCreatePlanEnterprise {
233246
 		h.renderPlanSelection(w, r, "Enterprise organizations are contact-sales only today.")
234247
 		return
235248
 	}
249
+	if !form.AcceptTerms {
250
+		h.renderNewForm(w, r, form.withoutToken(), "You must accept the terms to create an organization.")
251
+		return
252
+	}
236253
 	if form.GitHubOrg != "" {
237254
 		if _, err := orgs.NormalizeGitHubOrg(form.GitHubOrg); err != nil {
238255
 			h.renderNewForm(w, r, form, "GitHub organization must be a valid organization name or github.com organization URL.")
@@ -335,8 +352,9 @@ func orgCreateTitle(plan string) string {
335352
 
336353
 func (h *Handlers) renderPlanSelection(w http.ResponseWriter, r *http.Request, errMsg string) {
337354
 	if err := h.d.Render.RenderPage(w, r, "orgs/new_plan", map[string]any{
338
-		"Title": "Choose a plan",
339
-		"Error": errMsg,
355
+		"Title":             "Pick a plan for your organization",
356
+		"Error":             errMsg,
357
+		"BillingConfigured": h.billingConfigured(),
340358
 	}); err != nil {
341359
 		h.d.Logger.ErrorContext(r.Context(), "orgs: render", "tpl", "orgs/new_plan", "error", err)
342360
 	}
internal/web/static/css/shithub.cssmodified
@@ -1041,34 +1041,157 @@ code {
10411041
   color: var(--fg-muted);
10421042
   font-size: 0.9rem;
10431043
 }
1044
-.shithub-auth-plans {
1044
+.shithub-org-plan-page {
10451045
   max-width: 72rem;
1046
+  margin: 3rem auto;
1047
+  padding: 0 1rem 3rem;
10461048
 }
1047
-.shithub-auth-plans .shithub-auth-aside {
1048
-  text-align: left;
1049
+.shithub-org-plan-page h1 {
1050
+  margin: 0;
1051
+  font-size: 1.65rem;
1052
+  text-align: center;
1053
+}
1054
+.shithub-org-plan-kicker {
1055
+  margin: 0 0 0.25rem;
1056
+  color: var(--fg-muted);
1057
+  font-size: 0.75rem;
1058
+  letter-spacing: 0;
1059
+  text-align: center;
1060
+}
1061
+.shithub-org-plan-lede {
1062
+  max-width: 42rem;
1063
+  margin: 0.75rem auto 2rem;
1064
+  color: var(--fg-muted);
1065
+  text-align: center;
10491066
 }
10501067
 .shithub-billing-plan-grid {
10511068
   display: grid;
10521069
   grid-template-columns: repeat(3, minmax(0, 1fr));
1053
-  gap: 1rem;
1070
+  gap: 0.75rem;
10541071
   align-items: stretch;
10551072
 }
1056
-.shithub-billing-plan-grid .Box {
1073
+.shithub-pricing-card {
1074
+  position: relative;
1075
+  display: grid;
1076
+  grid-template-rows: auto auto auto 1fr;
1077
+  gap: 0.9rem;
1078
+  padding: 1.25rem;
10571079
   min-height: 100%;
1080
+  background: var(--canvas-default);
1081
+  border: 1px solid var(--border-default);
1082
+  border-radius: 6px;
10581083
 }
1059
-.shithub-billing-plan-grid .Box-body {
1060
-  display: grid;
1061
-  gap: 0.75rem;
1062
-  align-content: start;
1084
+.shithub-pricing-card.is-featured {
1085
+  border-color: var(--accent-fg);
1086
+  box-shadow: inset 0 3px 0 var(--accent-fg);
10631087
 }
10641088
 .shithub-billing-plan-grid .shithub-button {
10651089
   width: 100%;
10661090
   justify-content: center;
10671091
 }
1092
+.shithub-pricing-card-head h2 {
1093
+  margin: 0 0 0.25rem;
1094
+  font-size: 1.05rem;
1095
+}
1096
+.shithub-pricing-card-head p,
1097
+.shithub-pricing-note {
1098
+  margin: 0;
1099
+  color: var(--fg-muted);
1100
+  font-size: 0.85rem;
1101
+}
1102
+.shithub-pricing-badge {
1103
+  margin: -1.25rem -1.25rem 0;
1104
+  padding: 0.35rem 1rem;
1105
+  color: #fff;
1106
+  background: var(--accent-fg);
1107
+  border-radius: 6px 6px 0 0;
1108
+  font-size: 0.7rem;
1109
+  font-weight: 700;
1110
+  text-align: center;
1111
+  text-transform: uppercase;
1112
+}
1113
+.shithub-pricing-price {
1114
+  margin: 0;
1115
+  color: var(--fg-muted);
1116
+}
1117
+.shithub-pricing-price span {
1118
+  color: var(--fg-default);
1119
+  font-size: 2rem;
1120
+  font-weight: 600;
1121
+}
1122
+.shithub-pricing-price small {
1123
+  font-size: 0.8rem;
1124
+}
1125
+.shithub-pricing-features {
1126
+  display: grid;
1127
+  gap: 0.7rem;
1128
+  margin: 0;
1129
+  padding: 0;
1130
+  list-style: none;
1131
+  color: var(--fg-muted);
1132
+  font-size: 0.9rem;
1133
+}
1134
+.shithub-pricing-features li {
1135
+  display: grid;
1136
+  grid-template-columns: 16px 1fr;
1137
+  gap: 0.5rem;
1138
+}
1139
+.shithub-pricing-features svg {
1140
+  margin-top: 0.15rem;
1141
+  color: var(--fg-muted);
1142
+}
10681143
 .shithub-billing-plan-compare {
10691144
   margin-top: 1.5rem;
10701145
   overflow-x: auto;
10711146
 }
1147
+.shithub-plan-compare {
1148
+  margin-top: 3rem;
1149
+  overflow-x: auto;
1150
+}
1151
+.shithub-plan-compare h2 {
1152
+  margin: 0 0 1rem;
1153
+  text-align: center;
1154
+}
1155
+.shithub-org-setup {
1156
+  max-width: 38rem;
1157
+}
1158
+.shithub-org-setup .shithub-org-plan-kicker {
1159
+  text-align: left;
1160
+}
1161
+.shithub-org-owner-choice,
1162
+.shithub-org-import-create {
1163
+  display: grid;
1164
+  gap: 0.75rem;
1165
+  margin: 0;
1166
+  padding: 1rem;
1167
+  border: 1px solid var(--border-default);
1168
+  border-radius: 6px;
1169
+}
1170
+.shithub-org-owner-choice legend,
1171
+.shithub-org-import-create legend {
1172
+  padding: 0 0.25rem;
1173
+  font-weight: 600;
1174
+}
1175
+.shithub-org-owner-choice label,
1176
+.shithub-org-terms {
1177
+  display: grid;
1178
+  grid-template-columns: auto 1fr;
1179
+  gap: 0.5rem;
1180
+  align-items: start;
1181
+  font-weight: 400;
1182
+}
1183
+.shithub-org-owner-choice label small {
1184
+  display: block;
1185
+  margin-top: 0.15rem;
1186
+  color: var(--fg-muted);
1187
+}
1188
+.shithub-org-owner-choice label.is-disabled {
1189
+  color: var(--fg-muted);
1190
+}
1191
+.shithub-org-terms {
1192
+  color: var(--fg-muted);
1193
+  font-size: 0.9rem;
1194
+}
10721195
 .shithub-org-billing-compare {
10731196
   margin-top: 0;
10741197
 }
internal/web/templates/_nav.htmlmodified
@@ -45,7 +45,7 @@
4545
         <a role="menuitem" class="shithub-nav-action-item" href="/new">{{ octicon "repo" }} <span>New repository</span></a>
4646
         <span role="menuitem" aria-disabled="true" class="shithub-nav-action-item is-disabled">{{ octicon "upload" }} <span>Import repository</span></span>
4747
         <div class="shithub-nav-action-divider" role="separator"></div>
48
-        <a role="menuitem" class="shithub-nav-action-item" href="/organizations/new">{{ octicon "organization" }} <span>New organization</span></a>
48
+        <a role="menuitem" class="shithub-nav-action-item" href="/organizations/plan">{{ octicon "organization" }} <span>New organization</span></a>
4949
         <span role="menuitem" aria-disabled="true" class="shithub-nav-action-item is-disabled">{{ octicon "table" }} <span>New project</span></span>
5050
       </div>
5151
     </details>
internal/web/templates/explore/index.htmlmodified
@@ -35,7 +35,7 @@
3535
           </ol>
3636
           <div class="shithub-dashboard-identity-actions">
3737
             <a href="/settings/organizations">{{ octicon "organization" }} Manage organizations</a>
38
-            <a href="/organizations/new">{{ octicon "plus" }} Create organization</a>
38
+            <a href="/organizations/plan">{{ octicon "plus" }} Create organization</a>
3939
           </div>
4040
         </div>
4141
       </details>
internal/web/templates/orgs/new.htmlmodified
@@ -1,5 +1,6 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth">
2
+<section class="shithub-auth shithub-org-setup">
3
+  <p class="shithub-org-plan-kicker">Tell us about your organization</p>
34
   <h1>{{ if eq .Form.SelectedTier "team" }}Set up your organization{{ else }}Create an organization{{ end }}</h1>
45
   {{ if .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ .Error }}</p>{{ end }}
56
   <form method="POST" action="/organizations" novalidate>
@@ -23,6 +24,23 @@
2324
       <span>Billing email (optional)</span>
2425
       <input type="email" name="billing_email" autocomplete="email" value="{{ .Form.BillingEmail }}">
2526
     </label>
27
+    <fieldset class="shithub-org-owner-choice">
28
+      <legend>This organization belongs to:</legend>
29
+      <label>
30
+        <input type="radio" name="owner_account" value="personal" checked>
31
+        <span>
32
+          <strong>My personal account</strong>
33
+          <small>{{ .Viewer.Username }}</small>
34
+        </span>
35
+      </label>
36
+      <label class="is-disabled">
37
+        <input type="radio" name="owner_account" value="business" disabled>
38
+        <span>
39
+          <strong>A business or institution</strong>
40
+          <small>Business-owned organizations are planned for the Enterprise track.</small>
41
+        </span>
42
+      </label>
43
+    </fieldset>
2644
     <fieldset class="shithub-org-import-create">
2745
       <legend>Import repositories from GitHub</legend>
2846
       <p>Optionally mirror repositories from a GitHub organization after this organization is created.</p>
@@ -36,6 +54,10 @@
3654
       </label>
3755
       <p class="shithub-auth-aside">Without a token, shithub imports public repositories only. With a token, repositories visible to that token are imported and private repositories stay private.</p>
3856
     </fieldset>
57
+    <label class="shithub-org-terms">
58
+      <input type="checkbox" name="accept_terms" value="1"{{ if .Form.AcceptTerms }} checked{{ end }}>
59
+      <span>I accept the shithub Terms of Service and understand that organization billing is managed through hosted checkout when a paid plan is selected.</span>
60
+    </label>
3961
     <button type="submit" class="shithub-button shithub-button-primary">Create organization</button>
4062
   </form>
4163
   <p class="shithub-auth-aside">
internal/web/templates/orgs/new_plan.htmlmodified
@@ -1,90 +1,90 @@
11
 {{ define "page" -}}
2
-<section class="shithub-auth shithub-auth-plans">
2
+<section class="shithub-org-plan-page">
3
+  <p class="shithub-org-plan-kicker">Choose a plan</p>
34
   <h1>Pick a plan for your organization</h1>
4
-  <p class="shithub-auth-aside">Start free, or choose Team if you want paid organization features and hosted billing from the beginning.</p>
5
+  <p class="shithub-org-plan-lede">Start with Free for public and basic private collaboration, or choose Team when your organization needs paid controls from the beginning.</p>
56
   {{ if .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ .Error }}</p>{{ end }}
67
 
78
   <div class="shithub-billing-plan-grid">
8
-    <section class="Box">
9
-      <div class="Box-header"><h2 class="Box-title">Free</h2></div>
10
-      <div class="Box-body">
11
-        <p><strong>$0</strong> USD per user/month</p>
12
-        <p>Basic organizations for public work, visible teams, and standard collaboration.</p>
13
-        <p><a href="/organizations/new?plan=free" class="shithub-button">Create a free organization</a></p>
9
+    <section class="shithub-pricing-card">
10
+      <div class="shithub-pricing-card-head">
11
+        <h2>Free</h2>
12
+        <p>For individuals and small open-source organizations.</p>
1413
       </div>
14
+      <p class="shithub-pricing-price"><span>$0</span> USD <small>per user/month</small></p>
15
+      <p><a href="/organizations/new?plan=free" class="shithub-button">Create a free organization</a></p>
16
+      <ul class="shithub-pricing-features">
17
+        <li>{{ octicon "check" }} Public and private org repositories</li>
18
+        <li>{{ octicon "check" }} Org members and invitations</li>
19
+        <li>{{ octicon "check" }} Visible teams</li>
20
+        <li>{{ octicon "check" }} Basic branch protection</li>
21
+        <li>{{ octicon "dash" }} Upgrade for secret teams</li>
22
+      </ul>
1523
     </section>
1624
 
17
-    <section class="Box">
18
-      <div class="Box-header"><h2 class="Box-title">Team</h2></div>
19
-      <div class="Box-body">
20
-        <p><strong>$4</strong> USD per active member/month</p>
21
-        <p>Unlock secret teams, advanced private-repo branch protection, required reviewers, and org-level Actions secrets and variables.</p>
22
-        <p><a href="/organizations/new?plan=team" class="shithub-button shithub-button-primary">Continue with Team</a></p>
25
+    <section class="shithub-pricing-card is-featured">
26
+      <div class="shithub-pricing-badge">Most popular</div>
27
+      <div class="shithub-pricing-card-head">
28
+        <h2>Team</h2>
29
+        <p>For organizations that need paid collaboration controls.</p>
2330
       </div>
31
+      <p class="shithub-pricing-price"><span>$4</span> USD <small>per active member/month</small></p>
32
+      {{ if .BillingConfigured }}
33
+      <p><a href="/organizations/new?plan=team" class="shithub-button shithub-button-primary">Continue with Team</a></p>
34
+      {{ else }}
35
+      <p><button type="button" class="shithub-button shithub-button-primary" disabled>Continue with Team</button></p>
36
+      {{ if .Viewer.IsSiteAdmin }}
37
+      <p class="shithub-pricing-note">Stripe Billing is not fully configured. Set the secret key, webhook secret, and Team price ID before accepting Team checkout.</p>
38
+      {{ end }}
39
+      {{ end }}
40
+      <ul class="shithub-pricing-features">
41
+        <li>{{ octicon "check" }} Everything in Free</li>
42
+        <li>{{ octicon "check" }} Secret teams</li>
43
+        <li>{{ octicon "check" }} Advanced private-repo branch protection</li>
44
+        <li>{{ octicon "check" }} Required reviewers on private org repos</li>
45
+        <li>{{ octicon "check" }} Org-level Actions secrets and variables</li>
46
+      </ul>
2447
     </section>
2548
 
26
-    <section class="Box">
27
-      <div class="Box-header"><h2 class="Box-title">Enterprise</h2></div>
28
-      <div class="Box-body">
29
-        <p><strong>Contact sales</strong></p>
30
-        <p>Enterprise remains a stub until the hosted product grows into those promises.</p>
31
-        <p><a href="/organizations/new?plan=enterprise" class="shithub-button">Contact sales</a></p>
49
+    <section class="shithub-pricing-card">
50
+      <div class="shithub-pricing-card-head">
51
+        <h2>Enterprise</h2>
52
+        <p>For larger installations that need a contract and custom support.</p>
3253
       </div>
54
+      <p class="shithub-pricing-price"><span>$21</span> USD <small>starting point</small></p>
55
+      <p><a href="/organizations/new?plan=enterprise" class="shithub-button">Contact sales</a></p>
56
+      <ul class="shithub-pricing-features">
57
+        <li>{{ octicon "check" }} Contact-sales planning</li>
58
+        <li>{{ octicon "check" }} Future enterprise account structure</li>
59
+        <li>{{ octicon "check" }} Future compliance and support options</li>
60
+      </ul>
3361
     </section>
3462
   </div>
3563
 
36
-  <div class="Box shithub-billing-plan-compare">
37
-    <div class="Box-header"><h2 class="Box-title">Compare features</h2></div>
38
-    <div class="Box-body">
39
-      <table class="shithub-org-billing-table">
40
-        <thead>
41
-          <tr>
42
-            <th scope="col">Feature</th>
43
-            <th scope="col">Free</th>
44
-            <th scope="col">Team</th>
45
-            <th scope="col">Enterprise</th>
46
-          </tr>
47
-        </thead>
48
-        <tbody>
49
-          <tr>
50
-            <td>Public and private org repositories</td>
51
-            <td>Included</td>
52
-            <td>Included</td>
53
-            <td>Contact sales</td>
54
-          </tr>
55
-          <tr>
56
-            <td>Visible teams</td>
57
-            <td>Included</td>
58
-            <td>Included</td>
59
-            <td>Contact sales</td>
60
-          </tr>
61
-          <tr>
62
-            <td>Secret teams</td>
63
-            <td>Upgrade</td>
64
-            <td>Included</td>
65
-            <td>Contact sales</td>
66
-          </tr>
67
-          <tr>
68
-            <td>Advanced private-repo branch protection</td>
69
-            <td>Upgrade</td>
70
-            <td>Included</td>
71
-            <td>Contact sales</td>
72
-          </tr>
73
-          <tr>
74
-            <td>Required reviewers on private org repos</td>
75
-            <td>Upgrade</td>
76
-            <td>Included</td>
77
-            <td>Contact sales</td>
78
-          </tr>
79
-          <tr>
80
-            <td>Org-level Actions secrets and variables</td>
81
-            <td>Upgrade</td>
82
-            <td>Included</td>
83
-            <td>Contact sales</td>
84
-          </tr>
85
-        </tbody>
86
-      </table>
87
-    </div>
88
-  </div>
64
+  <section class="shithub-plan-compare" aria-labelledby="org-plan-compare-heading">
65
+    <h2 id="org-plan-compare-heading">Compare features</h2>
66
+    <table class="shithub-org-billing-table">
67
+      <thead>
68
+        <tr>
69
+          <th scope="col">Capability</th>
70
+          <th scope="col">Free</th>
71
+          <th scope="col">Team</th>
72
+          <th scope="col">Enterprise</th>
73
+        </tr>
74
+      </thead>
75
+      <tbody>
76
+        <tr><td>Public org repositories</td><td>Included</td><td>Included</td><td>Contact sales</td></tr>
77
+        <tr><td>Basic private org repositories</td><td>Included</td><td>Included</td><td>Contact sales</td></tr>
78
+        <tr><td>Org members and invitations</td><td>Included</td><td>Billed by active member</td><td>Contact sales</td></tr>
79
+        <tr><td>Visible teams</td><td>Included</td><td>Included</td><td>Contact sales</td></tr>
80
+        <tr><td>Secret teams</td><td>Upgrade</td><td>Included</td><td>Contact sales</td></tr>
81
+        <tr><td>Basic branch protection</td><td>Included</td><td>Included</td><td>Contact sales</td></tr>
82
+        <tr><td>Advanced private-repo branch protection</td><td>Upgrade</td><td>Included</td><td>Contact sales</td></tr>
83
+        <tr><td>Required reviewers on private org repos</td><td>Upgrade</td><td>Included</td><td>Contact sales</td></tr>
84
+        <tr><td>Org-level Actions secrets</td><td>Upgrade</td><td>Included</td><td>Contact sales</td></tr>
85
+        <tr><td>Org-level Actions variables</td><td>Upgrade</td><td>Included</td><td>Contact sales</td></tr>
86
+      </tbody>
87
+    </table>
88
+  </section>
8989
 </section>
9090
 {{- end }}
internal/web/templates/settings/organizations.htmlmodified
@@ -17,7 +17,7 @@
1717
     <section class="shithub-settings-orgs-section" aria-labelledby="settings-organizations-heading">
1818
       <div class="shithub-settings-orgs-head">
1919
         <h2 id="settings-organizations-heading">Organizations</h2>
20
-        <a href="/organizations/new" class="shithub-button shithub-button-primary">New organization</a>
20
+        <a href="/organizations/plan" class="shithub-button shithub-button-primary">New organization</a>
2121
       </div>
2222
 
2323
       {{ if .Organizations }}
@@ -46,7 +46,7 @@
4646
       {{ else }}
4747
       <div class="shithub-settings-org-empty">
4848
         <p>You are not a member of any organizations yet.</p>
49
-        <a href="/organizations/new" class="shithub-button shithub-button-primary">Create organization</a>
49
+        <a href="/organizations/plan" class="shithub-button shithub-button-primary">Create organization</a>
5050
       </div>
5151
       {{ end }}
5252
     </section>
@@ -54,7 +54,7 @@
5454
     <section class="shithub-settings-orgs-move" aria-labelledby="settings-move-org-heading">
5555
       <h2 id="settings-move-org-heading">Move to an organization</h2>
5656
       <p>Your personal account cannot be converted to an organization. You must create a new organization and transfer your repositories and projects to it instead. You can then rename your personal account and the organization if you want your organization to have the same name that you are currently using for your personal account.</p>
57
-      <a href="/organizations/new" class="shithub-button shithub-button-ghost">Move work to an organization</a>
57
+      <a href="/organizations/plan" class="shithub-button shithub-button-ghost">Move work to an organization</a>
5858
     </section>
5959
   </div>
6060
 </div>