Skip to content

Commit 7982ad7

Browse files
authored
feat: expose premium trial form via CLI (coder#15054)
This PR closes coder#14856
1 parent 78ff375 commit 7982ad7

File tree

6 files changed

+233
-9
lines changed

6 files changed

+233
-9
lines changed

.github/workflows/pr-deploy.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ jobs:
425425
--first-user-username coder \
426426
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
427427
--first-user-password $password \
428-
--first-user-trial \
428+
--first-user-trial=false \
429429
--use-token-as-session \
430430
https://${{ env.PR_HOSTNAME }}
431431

cli/login.go

+124-5
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,59 @@ func (r *RootCmd) login() *serpent.Command {
267267
trial = v == "yes" || v == "y"
268268
}
269269

270+
var trialInfo codersdk.CreateFirstUserTrialInfo
271+
if trial {
272+
if trialInfo.FirstName == "" {
273+
trialInfo.FirstName, err = promptTrialInfo(inv, "firstName")
274+
if err != nil {
275+
return err
276+
}
277+
}
278+
if trialInfo.LastName == "" {
279+
trialInfo.LastName, err = promptTrialInfo(inv, "lastName")
280+
if err != nil {
281+
return err
282+
}
283+
}
284+
if trialInfo.PhoneNumber == "" {
285+
trialInfo.PhoneNumber, err = promptTrialInfo(inv, "phoneNumber")
286+
if err != nil {
287+
return err
288+
}
289+
}
290+
if trialInfo.JobTitle == "" {
291+
trialInfo.JobTitle, err = promptTrialInfo(inv, "jobTitle")
292+
if err != nil {
293+
return err
294+
}
295+
}
296+
if trialInfo.CompanyName == "" {
297+
trialInfo.CompanyName, err = promptTrialInfo(inv, "companyName")
298+
if err != nil {
299+
return err
300+
}
301+
}
302+
if trialInfo.Country == "" {
303+
trialInfo.Country, err = promptCountry(inv)
304+
if err != nil {
305+
return err
306+
}
307+
}
308+
if trialInfo.Developers == "" {
309+
trialInfo.Developers, err = promptDevelopers(inv)
310+
if err != nil {
311+
return err
312+
}
313+
}
314+
}
315+
270316
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
271-
Email: email,
272-
Username: username,
273-
Name: name,
274-
Password: password,
275-
Trial: trial,
317+
Email: email,
318+
Username: username,
319+
Name: name,
320+
Password: password,
321+
Trial: trial,
322+
TrialInfo: trialInfo,
276323
})
277324
if err != nil {
278325
return xerrors.Errorf("create initial user: %w", err)
@@ -449,3 +496,75 @@ func openURL(inv *serpent.Invocation, urlToOpen string) error {
449496

450497
return browser.OpenURL(urlToOpen)
451498
}
499+
500+
func promptTrialInfo(inv *serpent.Invocation, fieldName string) (string, error) {
501+
value, err := cliui.Prompt(inv, cliui.PromptOptions{
502+
Text: fmt.Sprintf("Please enter %s:", pretty.Sprint(cliui.DefaultStyles.Field, fieldName)),
503+
Validate: func(s string) error {
504+
if strings.TrimSpace(s) == "" {
505+
return xerrors.Errorf("%s is required", fieldName)
506+
}
507+
return nil
508+
},
509+
})
510+
if err != nil {
511+
if errors.Is(err, cliui.Canceled) {
512+
return "", nil
513+
}
514+
return "", err
515+
}
516+
return value, nil
517+
}
518+
519+
func promptDevelopers(inv *serpent.Invocation) (string, error) {
520+
options := []string{"1-100", "101-500", "501-1000", "1001-2500", "2500+"}
521+
selection, err := cliui.Select(inv, cliui.SelectOptions{
522+
Options: options,
523+
HideSearch: false,
524+
Message: "Select the number of developers:",
525+
})
526+
if err != nil {
527+
return "", xerrors.Errorf("select developers: %w", err)
528+
}
529+
return selection, nil
530+
}
531+
532+
func promptCountry(inv *serpent.Invocation) (string, error) {
533+
countries := []string{
534+
"Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda",
535+
"Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados",
536+
"Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia, Plurinational State of", "Bonaire, Sint Eustatius and Saba", "Bosnia and Herzegovina", "Botswana",
537+
"Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada",
538+
"Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros",
539+
"Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Côte d'Ivoire", "Croatia", "Cuba", "Curaçao", "Cyprus", "Czech Republic",
540+
"Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia",
541+
"Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon",
542+
"Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam",
543+
"Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong",
544+
"Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy",
545+
"Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait",
546+
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
547+
"Macao", "Macedonia, the Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
548+
"Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montenegro", "Montserrat",
549+
"Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Caledonia", "New Zealand", "Nicaragua",
550+
"Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestine, State of",
551+
"Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar",
552+
"Réunion", "Romania", "Russian Federation", "Rwanda", "Saint Barthélemy", "Saint Helena, Ascension and Tristan da Cunha", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin (French part)", "Saint Pierre and Miquelon",
553+
"Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
554+
"Sint Maarten (Dutch part)", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Sudan", "Spain", "Sri Lanka",
555+
"Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of",
556+
"Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands",
557+
"Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
558+
"Venezuela, Bolivarian Republic of", "Vietnam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe",
559+
}
560+
561+
selection, err := cliui.Select(inv, cliui.SelectOptions{
562+
Options: countries,
563+
Message: "Select the country:",
564+
HideSearch: false,
565+
})
566+
if err != nil {
567+
return "", xerrors.Errorf("select country: %w", err)
568+
}
569+
return selection, nil
570+
}

cli/login_test.go

+96-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,58 @@ func TestLogin(t *testing.T) {
9696
"password", coderdtest.FirstUserParams.Password,
9797
"password", coderdtest.FirstUserParams.Password, // confirm
9898
"trial", "yes",
99+
"firstName", coderdtest.TrialUserParams.FirstName,
100+
"lastName", coderdtest.TrialUserParams.LastName,
101+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
102+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
103+
"companyName", coderdtest.TrialUserParams.CompanyName,
104+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
105+
}
106+
for i := 0; i < len(matches); i += 2 {
107+
match := matches[i]
108+
value := matches[i+1]
109+
pty.ExpectMatch(match)
110+
pty.WriteLine(value)
111+
}
112+
pty.ExpectMatch("Welcome to Coder")
113+
<-doneChan
114+
ctx := testutil.Context(t, testutil.WaitShort)
115+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
116+
Email: coderdtest.FirstUserParams.Email,
117+
Password: coderdtest.FirstUserParams.Password,
118+
})
119+
require.NoError(t, err)
120+
client.SetSessionToken(resp.SessionToken)
121+
me, err := client.User(ctx, codersdk.Me)
122+
require.NoError(t, err)
123+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
124+
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
125+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
126+
})
127+
128+
t.Run("InitialUserTTYWithNoTrial", func(t *testing.T) {
129+
t.Parallel()
130+
client := coderdtest.New(t, nil)
131+
// The --force-tty flag is required on Windows, because the `isatty` library does not
132+
// accurately detect Windows ptys when they are not attached to a process:
133+
// https://github.com/mattn/go-isatty/issues/59
134+
doneChan := make(chan struct{})
135+
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
136+
pty := ptytest.New(t).Attach(root)
137+
go func() {
138+
defer close(doneChan)
139+
err := root.Run()
140+
assert.NoError(t, err)
141+
}()
142+
143+
matches := []string{
144+
"first user?", "yes",
145+
"username", coderdtest.FirstUserParams.Username,
146+
"name", coderdtest.FirstUserParams.Name,
147+
"email", coderdtest.FirstUserParams.Email,
148+
"password", coderdtest.FirstUserParams.Password,
149+
"password", coderdtest.FirstUserParams.Password, // confirm
150+
"trial", "no",
99151
}
100152
for i := 0; i < len(matches); i += 2 {
101153
match := matches[i]
@@ -142,6 +194,12 @@ func TestLogin(t *testing.T) {
142194
"password", coderdtest.FirstUserParams.Password,
143195
"password", coderdtest.FirstUserParams.Password, // confirm
144196
"trial", "yes",
197+
"firstName", coderdtest.TrialUserParams.FirstName,
198+
"lastName", coderdtest.TrialUserParams.LastName,
199+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
200+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
201+
"companyName", coderdtest.TrialUserParams.CompanyName,
202+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
145203
}
146204
for i := 0; i < len(matches); i += 2 {
147205
match := matches[i]
@@ -185,6 +243,12 @@ func TestLogin(t *testing.T) {
185243
"password", coderdtest.FirstUserParams.Password,
186244
"password", coderdtest.FirstUserParams.Password, // confirm
187245
"trial", "yes",
246+
"firstName", coderdtest.TrialUserParams.FirstName,
247+
"lastName", coderdtest.TrialUserParams.LastName,
248+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
249+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
250+
"companyName", coderdtest.TrialUserParams.CompanyName,
251+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
188252
}
189253
for i := 0; i < len(matches); i += 2 {
190254
match := matches[i]
@@ -220,6 +284,17 @@ func TestLogin(t *testing.T) {
220284
)
221285
pty := ptytest.New(t).Attach(inv)
222286
w := clitest.StartWithWaiter(t, inv)
287+
pty.ExpectMatch("firstName")
288+
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
289+
pty.ExpectMatch("lastName")
290+
pty.WriteLine(coderdtest.TrialUserParams.LastName)
291+
pty.ExpectMatch("phoneNumber")
292+
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
293+
pty.ExpectMatch("jobTitle")
294+
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
295+
pty.ExpectMatch("companyName")
296+
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
297+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
223298
pty.ExpectMatch("Welcome to Coder")
224299
w.RequireSuccess()
225300
ctx := testutil.Context(t, testutil.WaitShort)
@@ -248,6 +323,17 @@ func TestLogin(t *testing.T) {
248323
)
249324
pty := ptytest.New(t).Attach(inv)
250325
w := clitest.StartWithWaiter(t, inv)
326+
pty.ExpectMatch("firstName")
327+
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
328+
pty.ExpectMatch("lastName")
329+
pty.WriteLine(coderdtest.TrialUserParams.LastName)
330+
pty.ExpectMatch("phoneNumber")
331+
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
332+
pty.ExpectMatch("jobTitle")
333+
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
334+
pty.ExpectMatch("companyName")
335+
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
336+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
251337
pty.ExpectMatch("Welcome to Coder")
252338
w.RequireSuccess()
253339
ctx := testutil.Context(t, testutil.WaitShort)
@@ -299,12 +385,21 @@ func TestLogin(t *testing.T) {
299385
// Validate that we reprompt for matching passwords.
300386
pty.ExpectMatch("Passwords do not match")
301387
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
302-
303388
pty.WriteLine(coderdtest.FirstUserParams.Password)
304389
pty.ExpectMatch("Confirm")
305390
pty.WriteLine(coderdtest.FirstUserParams.Password)
306391
pty.ExpectMatch("trial")
307392
pty.WriteLine("yes")
393+
pty.ExpectMatch("firstName")
394+
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
395+
pty.ExpectMatch("lastName")
396+
pty.WriteLine(coderdtest.TrialUserParams.LastName)
397+
pty.ExpectMatch("phoneNumber")
398+
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
399+
pty.ExpectMatch("jobTitle")
400+
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
401+
pty.ExpectMatch("companyName")
402+
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
308403
pty.ExpectMatch("Welcome to Coder")
309404
<-doneChan
310405
})

coderd/coderdtest/coderdtest.go

+10
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,16 @@ var FirstUserParams = codersdk.CreateFirstUserRequest{
653653
Name: "Test User",
654654
}
655655

656+
var TrialUserParams = codersdk.CreateFirstUserTrialInfo{
657+
FirstName: "John",
658+
LastName: "Doe",
659+
PhoneNumber: "9999999999",
660+
JobTitle: "Engineer",
661+
CompanyName: "Acme Inc",
662+
Country: "United States",
663+
Developers: "10-50",
664+
}
665+
656666
// CreateFirstUser creates a user with preset credentials and authenticates
657667
// with the passed in codersdk client.
658668
func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirstUserResponse {

scripts/develop.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ fatal() {
164164

165165
if [ ! -f "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup" ]; then
166166
# Try to create the initial admin user.
167-
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=true; then
167+
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=false; then
168168
# Only create this file if an admin user was successfully
169169
# created, otherwise we won't retry on a later attempt.
170170
touch "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup"

site/src/pages/SetupPage/countries.tsx

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)