commit a64833ae44f768414babbc9db96452078a83dc47 equwal <truex@equwal.com> 2026-09-22 16:42:38 -0700 Home and account pages: links to all payment pages SBM_TEAMS_LARGE_URL pre-sells team bookmarks for 11 people or more. The home page shows it next to SBM_TEAMS_URL. SBM_SUPPORT_URL is the page of a supporter subscription. The home page and the account page link to it. The server does not start when one of these values is not an http or https address.
README.md | 7 +++++-- main.go | 51 +++++++++++++++++++++++++++++++-------------------- pages.html | 9 +++++++-- server_test.go | 28 ++++++++++++++++++++++------ 4 files changed, 65 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md index d9e4a53..64ec000 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,11 @@ All settings come from the environment. With Docker, put them in `.env`. SBM_ADDR address to listen on (default 127.0.0.1:8750) SBM_DATA data directory (default ./data) SBM_CONTACT email address on the privacy page (optional) - SBM_TEAMS_URL page that pre-sells team bookmarks (optional): the - home page links to it + SBM_TEAMS_URL page that pre-sells team bookmarks for up to 10 + people (optional): the home page links to it + SBM_TEAMS_LARGE_URL the same for teams of 11 people or more + SBM_SUPPORT_URL page of a supporter subscription (optional): the home + page and the account page link to it SBM_TRIAL_DAYS days of sync before payment, with billing on (default 30) SBM_BILLING_START date when billing starts, as 2026-10-01: accounts from before it get the full trial from that date diff --git a/main.go b/main.go index f84950a..9f4584d 100644 --- a/main.go +++ b/main.go @@ -41,10 +41,12 @@ type server struct { trial time.Duration since time.Time // start of billing: no trial ends before since + trial contact string - teams string // address of the page that pre-sells team bookmarks, or "" - prices map[string]string // plan ("month", "year"): Stripe price ID - labels map[string]string // plan: price as text - limit limiter + // Payment pages, or "": the pre-order of team bookmarks for up to 10 + // people, the same for 11 people or more, and a supporter subscription. + teams, teamsLarge, support string + prices map[string]string // plan ("month", "year"): Stripe price ID + labels map[string]string // plan: price as text + limit limiter // mail sends an email. It is nil when the server has no SMTP server: // then accounts need no email check. mail func(to, subject, body string) error @@ -66,25 +68,23 @@ func main() { log.Fatalf("SBM_BILLING_START: %v", err) } } - teams := os.Getenv("SBM_TEAMS_URL") - if t, err := url.Parse(teams); teams != "" && (err != nil || t.Host == "" || (t.Scheme != "https" && t.Scheme != "http")) { - log.Fatalf("SBM_TEAMS_URL: not an address: %q", teams) - } store, err := openStore(env("SBM_DATA", "data")) if err != nil { log.Fatal(err) } s := &server{ - store: store, - site: site, - origin: u.Scheme + "://" + u.Host, - secure: u.Scheme == "https", - trial: time.Duration(days) * 24 * time.Hour, - since: since, - contact: os.Getenv("SBM_CONTACT"), - teams: teams, - prices: map[string]string{"month": os.Getenv("STRIPE_PRICE_MONTH"), "year": os.Getenv("STRIPE_PRICE_YEAR")}, - labels: map[string]string{"month": "monthly", "year": "yearly"}, + store: store, + site: site, + origin: u.Scheme + "://" + u.Host, + secure: u.Scheme == "https", + trial: time.Duration(days) * 24 * time.Hour, + since: since, + contact: os.Getenv("SBM_CONTACT"), + teams: link("SBM_TEAMS_URL"), + teamsLarge: link("SBM_TEAMS_LARGE_URL"), + support: link("SBM_SUPPORT_URL"), + prices: map[string]string{"month": os.Getenv("STRIPE_PRICE_MONTH"), "year": os.Getenv("STRIPE_PRICE_YEAR")}, + labels: map[string]string{"month": "monthly", "year": "yearly"}, } if key := os.Getenv("STRIPE_SECRET_KEY"); key != "" { for _, v := range []string{"STRIPE_WEBHOOK_SECRET", "STRIPE_PRICE_MONTH", "STRIPE_PRICE_YEAR"} { @@ -134,6 +134,16 @@ func env(name, fallback string) string { return fallback } +// link gives the address in the environment variable name, or "". The +// server does not start when the value is not an http or https address. +func link(name string) string { + v := os.Getenv(name) + if u, err := url.Parse(v); v != "" && (err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http")) { + log.Fatalf("%s: not an address: %q", name, v) + } + return v +} + func (s *server) routes() http.Handler { m := http.NewServeMux() m.HandleFunc("GET /{$}", s.home) @@ -171,7 +181,7 @@ func (s *server) routes() http.Handler { type page struct { Title, Error, Email, State, URL, Contact string - Teams string // address of the pre-order page + Teams, TeamsLarge, Support string // addresses of payment pages Month, Year string TrialDays int Billing, CanSubscribe, Unconfirmed bool @@ -179,7 +189,8 @@ type page struct { } func (s *server) page(title string) page { - return page{Title: title, URL: s.site, Contact: s.contact, Teams: s.teams, Billing: s.stripe != nil, + return page{Title: title, URL: s.site, Contact: s.contact, Billing: s.stripe != nil, + Teams: s.teams, TeamsLarge: s.teamsLarge, Support: s.support, Month: s.labels["month"], Year: s.labels["year"], TrialDays: int(s.trial.Hours() / 24)} } diff --git a/pages.html b/pages.html index 2e9b62f..5d69f22 100644 --- a/pages.html +++ b/pages.html @@ -38,6 +38,9 @@ button { font: inherit; padding: .4em 1em; margin: .2em .5em .2em 0; } </html> {{end}} +{{define "support"}}{{with .Support}}<p>sbm Sync is free. To pay for the server and the work on sbm, +<a href="{{.}}">become a supporter</a>.</p>{{end}}{{end}} + {{define "home"}}{{template "head" .}} <h1>Your bookmarks, the same everywhere</h1> <p><a href="https://github.com/equwal/sbm">sbm</a> keeps your bookmarks in one plain @@ -48,8 +51,9 @@ Chrome.</p> <p>{{if .Email}}<a href="/account">Your account</a>{{else}}<a href="/signup">Create an account</a> or <a href="/login">sign in</a>.{{end}}</p> {{with .Teams}}<h2>Teams</h2> <p>Coming: bookmarks for teams. The members of a team share one bookmark -file, and sync it on each of their devices. <a href="{{.}}">Pre-order the -founding plan</a>.</p>{{end}} +file, and sync it on each of their devices. Pre-order the founding plan: <a href="{{.}}">for up to 10 people</a>{{with $.TeamsLarge}}, +or <a href="{{.}}">for 11 people or more</a>{{end}}.</p>{{end}} +{{template "support" .}} <p class="note">sbm Sync is free software under the AGPL-3.0. You can <a href="https://github.com/equwal/sbm-sync">run your own server</a>.</p> {{template "foot" .}}{{end}} @@ -89,6 +93,7 @@ email that this server sent to {{.Email}}. Sync starts after that.</p> <button name="plan" value="month">Subscribe: {{.Month}}</button> </form>{{end}} {{if .Customer}}<form method="post" action="/account/portal"><button>Manage billing</button></form>{{end}} +{{template "support" .}} <h2>Connect your devices</h2> <p>Sign in with this email and password on each device:</p> diff --git a/server_test.go b/server_test.go index fc04d93..8f50473 100644 --- a/server_test.go +++ b/server_test.go @@ -498,14 +498,30 @@ func TestOldVersionsGo(t *testing.T) { } } -func TestTeamsPreorderLink(t *testing.T) { +func TestPaymentLinks(t *testing.T) { e := newEnv(t, false) - if strings.Contains(e.body(http.DefaultClient, "/"), "Pre-order") { - t.Error("the home page offers teams without SBM_TEAMS_URL") + c := e.signup("me@example.org") + if home := e.body(c, "/"); strings.Contains(home, "Pre-order") || strings.Contains(home, "supporter") { + t.Error("the home page offers something without the settings") + } + // The link for large teams needs the link for small teams. + e.s.teamsLarge = "https://buy.stripe.com/large" + if strings.Contains(e.body(c, "/"), "large") { + t.Error("the home page links only to large teams") + } + e.s.teams, e.s.support = "https://buy.stripe.com/small", "https://buy.stripe.com/support" + home := e.body(c, "/") + for _, want := range []string{ + `Pre-order the founding plan: <a href="https://buy.stripe.com/small">for up to 10 people</a>`, + `<a href="https://buy.stripe.com/large">for 11 people or more</a>`, + `<a href="https://buy.stripe.com/support">become a supporter</a>`, + } { + if !strings.Contains(home, want) { + t.Errorf("the home page lacks %q", want) + } } - e.s.teams = "https://buy.stripe.com/test_x" - if !strings.Contains(e.body(http.DefaultClient, "/"), `<a href="https://buy.stripe.com/test_x">Pre-order`) { - t.Error("the home page has no pre-order link") + if !strings.Contains(e.body(c, "/account"), `<a href="https://buy.stripe.com/support">become a supporter</a>`) { + t.Error("the account page has no supporter link") } }