web, satellite: allow registering business accounts to ask for contact from sales team

Full prefix: web/satellite, satellite/{console, analytics, satellitedb}

- checkbox added to register view - business tab
- user being saved with new column
- add sales contact choice to Segment calls
- ui fix added to employee count dropdown

Change-Id: Ib976872463b88874ea9714db635d58c79cdbe3a1
This commit is contained in:
Malcolm Bouzi 2021-04-27 20:40:03 +02:00
parent 2cf10a7bf4
commit 136af8e630
11 changed files with 122 additions and 79 deletions

View File

@ -79,14 +79,15 @@ const (
// TrackCreateUserFields contains input data for tracking a create user event. // TrackCreateUserFields contains input data for tracking a create user event.
type TrackCreateUserFields struct { type TrackCreateUserFields struct {
ID uuid.UUID ID uuid.UUID
AnonymousID string AnonymousID string
FullName string FullName string
Email string Email string
Type UserType Type UserType
EmployeeCount string EmployeeCount string
CompanyName string CompanyName string
JobTitle string JobTitle string
HaveSalesContact bool
} }
func (service *Service) enqueueMessage(message segment.Message) { func (service *Service) enqueueMessage(message segment.Message) {
@ -122,6 +123,7 @@ func (service *Service) TrackCreateUser(fields TrackCreateUserFields) {
props.Set("company_size", fields.EmployeeCount) props.Set("company_size", fields.EmployeeCount)
props.Set("company_name", fields.CompanyName) props.Set("company_name", fields.CompanyName)
props.Set("job_title", fields.JobTitle) props.Set("job_title", fields.JobTitle)
props.Set("have_sales_contact", fields.HaveSalesContact)
} }
service.enqueueMessage(segment.Track{ service.enqueueMessage(segment.Track{

View File

@ -112,18 +112,19 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
var registerData struct { var registerData struct {
FullName string `json:"fullName"` FullName string `json:"fullName"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
Email string `json:"email"` Email string `json:"email"`
Partner string `json:"partner"` Partner string `json:"partner"`
PartnerID string `json:"partnerId"` PartnerID string `json:"partnerId"`
Password string `json:"password"` Password string `json:"password"`
SecretInput string `json:"secret"` SecretInput string `json:"secret"`
ReferrerUserID string `json:"referrerUserId"` ReferrerUserID string `json:"referrerUserId"`
IsProfessional bool `json:"isProfessional"` IsProfessional bool `json:"isProfessional"`
Position string `json:"position"` Position string `json:"position"`
CompanyName string `json:"companyName"` CompanyName string `json:"companyName"`
EmployeeCount string `json:"employeeCount"` EmployeeCount string `json:"employeeCount"`
HaveSalesContact bool `json:"haveSalesContact"`
} }
err = json.NewDecoder(r.Body).Decode(&registerData) err = json.NewDecoder(r.Body).Decode(&registerData)
@ -149,15 +150,16 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
user, err := a.service.CreateUser(ctx, user, err := a.service.CreateUser(ctx,
console.CreateUser{ console.CreateUser{
FullName: registerData.FullName, FullName: registerData.FullName,
ShortName: registerData.ShortName, ShortName: registerData.ShortName,
Email: registerData.Email, Email: registerData.Email,
PartnerID: registerData.PartnerID, PartnerID: registerData.PartnerID,
Password: registerData.Password, Password: registerData.Password,
IsProfessional: registerData.IsProfessional, IsProfessional: registerData.IsProfessional,
Position: registerData.Position, Position: registerData.Position,
CompanyName: registerData.CompanyName, CompanyName: registerData.CompanyName,
EmployeeCount: registerData.EmployeeCount, EmployeeCount: registerData.EmployeeCount,
HaveSalesContact: registerData.HaveSalesContact,
}, },
secret, secret,
) )
@ -178,6 +180,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
trackCreateUserFields.EmployeeCount = user.EmployeeCount trackCreateUserFields.EmployeeCount = user.EmployeeCount
trackCreateUserFields.CompanyName = user.CompanyName trackCreateUserFields.CompanyName = user.CompanyName
trackCreateUserFields.JobTitle = user.Position trackCreateUserFields.JobTitle = user.Position
trackCreateUserFields.HaveSalesContact = user.HaveSalesContact
} }
a.analytics.TrackCreateUser(trackCreateUserFields) a.analytics.TrackCreateUser(trackCreateUserFields)
@ -250,16 +253,17 @@ func (a *Auth) GetAccount(w http.ResponseWriter, r *http.Request) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
var user struct { var user struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
Email string `json:"email"` Email string `json:"email"`
PartnerID uuid.UUID `json:"partnerId"` PartnerID uuid.UUID `json:"partnerId"`
ProjectLimit int `json:"projectLimit"` ProjectLimit int `json:"projectLimit"`
IsProfessional bool `json:"isProfessional"` IsProfessional bool `json:"isProfessional"`
Position string `json:"position"` Position string `json:"position"`
CompanyName string `json:"companyName"` CompanyName string `json:"companyName"`
EmployeeCount string `json:"employeeCount"` EmployeeCount string `json:"employeeCount"`
HaveSalesContact bool `json:"haveSalesContact"`
} }
auth, err := console.GetAuth(ctx) auth, err := console.GetAuth(ctx)
@ -278,6 +282,7 @@ func (a *Auth) GetAccount(w http.ResponseWriter, r *http.Request) {
user.CompanyName = auth.User.CompanyName user.CompanyName = auth.User.CompanyName
user.Position = auth.User.Position user.Position = auth.User.Position
user.EmployeeCount = auth.User.EmployeeCount user.EmployeeCount = auth.User.EmployeeCount
user.HaveSalesContact = auth.User.HaveSalesContact
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&user) err = json.NewEncoder(w).Encode(&user)

View File

@ -561,17 +561,19 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
} }
newUser := &User{ newUser := &User{
ID: userID, ID: userID,
Email: user.Email, Email: user.Email,
FullName: user.FullName, FullName: user.FullName,
ShortName: user.ShortName, ShortName: user.ShortName,
PasswordHash: hash, PasswordHash: hash,
Status: Inactive, Status: Inactive,
IsProfessional: user.IsProfessional, IsProfessional: user.IsProfessional,
Position: user.Position, Position: user.Position,
CompanyName: user.CompanyName, CompanyName: user.CompanyName,
EmployeeCount: user.EmployeeCount, EmployeeCount: user.EmployeeCount,
HaveSalesContact: user.HaveSalesContact,
} }
if user.PartnerID != "" { if user.PartnerID != "" {
newUser.PartnerID, err = uuid.FromString(user.PartnerID) newUser.PartnerID, err = uuid.FromString(user.PartnerID)
if err != nil { if err != nil {

View File

@ -49,16 +49,17 @@ func (user *UserInfo) IsValid() error {
// CreateUser struct holds info for User creation. // CreateUser struct holds info for User creation.
type CreateUser struct { type CreateUser struct {
FullName string `json:"fullName"` FullName string `json:"fullName"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
Email string `json:"email"` Email string `json:"email"`
PartnerID string `json:"partnerId"` PartnerID string `json:"partnerId"`
Password string `json:"password"` Password string `json:"password"`
IsProfessional bool `json:"isProfessional"` IsProfessional bool `json:"isProfessional"`
Position string `json:"position"` Position string `json:"position"`
CompanyName string `json:"companyName"` CompanyName string `json:"companyName"`
WorkingOn string `json:"workingOn"` WorkingOn string `json:"workingOn"`
EmployeeCount string `json:"employeeCount"` EmployeeCount string `json:"employeeCount"`
HaveSalesContact bool `json:"haveSalesContact"`
} }
// IsValid checks CreateUser validity and returns error describing whats wrong. // IsValid checks CreateUser validity and returns error describing whats wrong.
@ -117,4 +118,6 @@ type User struct {
CompanySize int `json:"companySize"` CompanySize int `json:"companySize"`
WorkingOn string `json:"workingOn"` WorkingOn string `json:"workingOn"`
EmployeeCount string `json:"employeeCount"` EmployeeCount string `json:"employeeCount"`
HaveSalesContact bool `json:"haveSalesContact"`
} }

View File

@ -68,6 +68,7 @@ func (users *users) Insert(ctx context.Context, user *console.User) (_ *console.
optional.CompanyName = dbx.User_CompanyName(user.CompanyName) optional.CompanyName = dbx.User_CompanyName(user.CompanyName)
optional.WorkingOn = dbx.User_WorkingOn(user.WorkingOn) optional.WorkingOn = dbx.User_WorkingOn(user.WorkingOn)
optional.EmployeeCount = dbx.User_EmployeeCount(user.EmployeeCount) optional.EmployeeCount = dbx.User_EmployeeCount(user.EmployeeCount)
optional.HaveSalesContact = dbx.User_HaveSalesContact(user.HaveSalesContact)
} }
createdUser, err := users.db.Create_User(ctx, createdUser, err := users.db.Create_User(ctx,
@ -150,14 +151,15 @@ func userFromDBX(ctx context.Context, user *dbx.User) (_ *console.User, err erro
} }
result := console.User{ result := console.User{
ID: id, ID: id,
FullName: user.FullName, FullName: user.FullName,
Email: user.Email, Email: user.Email,
PasswordHash: user.PasswordHash, PasswordHash: user.PasswordHash,
Status: console.UserStatus(user.Status), Status: console.UserStatus(user.Status),
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
ProjectLimit: user.ProjectLimit, ProjectLimit: user.ProjectLimit,
IsProfessional: user.IsProfessional, IsProfessional: user.IsProfessional,
HaveSalesContact: user.HaveSalesContact,
} }
if user.PartnerId != nil { if user.PartnerId != nil {

View File

@ -205,7 +205,7 @@ export class AuthHttpApi {
* @returns id of created user * @returns id of created user
* @throws Error * @throws Error
*/ */
public async register(user: {fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string; isProfessional: boolean; position: string; companyName: string; employeeCount: string}, secret: string): Promise<string> { public async register(user: {fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string; isProfessional: boolean; position: string; companyName: string; employeeCount: string; haveSalesContact: boolean }, secret: string): Promise<string> {
const path = `${this.ROOT_PATH}/register`; const path = `${this.ROOT_PATH}/register`;
const body = { const body = {
secret: secret, secret: secret,
@ -219,8 +219,8 @@ export class AuthHttpApi {
position: user.position, position: user.position,
companyName: user.companyName, companyName: user.companyName,
employeeCount: user.employeeCount, employeeCount: user.employeeCount,
haveSalesContact: user.haveSalesContact,
}; };
const response = await this.http.post(path, JSON.stringify(body)); const response = await this.http.post(path, JSON.stringify(body));
if (!response.ok) { if (!response.ok) {
switch (response.status) { switch (response.status) {

View File

@ -252,8 +252,8 @@ export default class HeaderlessInput extends Vue {
&__options-wrapper { &__options-wrapper {
border: 1px solid rgba(56, 75, 101, 0.4); border: 1px solid rgba(56, 75, 101, 0.4);
position: absolute; position: absolute;
width: calc(100% - 4px); width: calc(100% - 5px);
top: 86px; top: 70px;
padding: 0; padding: 0;
background: #fff; background: #fff;
z-index: 21; z-index: 21;

View File

@ -38,6 +38,7 @@ export class User {
public position: string = '', public position: string = '',
public companyName: string = '', public companyName: string = '',
public employeeCount: string = '', public employeeCount: string = '',
public haveSalesContact: boolean = false,
) {} ) {}
public getFullName(): string { public getFullName(): string {

View File

@ -65,6 +65,7 @@ export default class RegisterArea extends Vue {
private isTermsAcceptedError: boolean = false; private isTermsAcceptedError: boolean = false;
private isLoading: boolean = false; private isLoading: boolean = false;
private isProfessional: boolean = false; private isProfessional: boolean = false;
private haveSalesContact: boolean = false;
private readonly auth: AuthHttpApi = new AuthHttpApi(); private readonly auth: AuthHttpApi = new AuthHttpApi();
@ -328,6 +329,8 @@ export default class RegisterArea extends Vue {
*/ */
private async createUser(): Promise<void> { private async createUser(): Promise<void> {
this.user.isProfessional = this.isProfessional; this.user.isProfessional = this.isProfessional;
this.user.haveSalesContact = this.haveSalesContact;
try { try {
this.userId = await this.auth.register(this.user, this.secret); this.userId = await this.auth.register(this.user, this.secret);
LocalData.setUserId(this.userId); LocalData.setUserId(this.userId);

View File

@ -137,17 +137,28 @@
/> />
</div> </div>
<AddCouponCodeInput v-if="couponCodeUIEnabled" /> <AddCouponCodeInput v-if="couponCodeUIEnabled" />
<div class="register-area__content-area__container__terms-area"> <div v-if="isProfessional" class="register-area__content-area__container__checkbox-area">
<label class="container">
<input type="checkbox" v-model="haveSalesContact">
<span class="checkmark"></span>
</label>
<label class="register-area__content-area__container__checkbox-area__msg-box" for="terms">
<p class="register-area__content-area__container__checkbox-area__msg-box__msg">
Please have the Sales Team contact me
</p>
</label>
</div>
<div class="register-area__content-area__container__checkbox-area">
<label class="container"> <label class="container">
<input id="terms" type="checkbox" v-model="isTermsAccepted"> <input id="terms" type="checkbox" v-model="isTermsAccepted">
<span class="checkmark" :class="{'error': isTermsAcceptedError}"></span> <span class="checkmark" :class="{'error': isTermsAcceptedError}"></span>
</label> </label>
<label class="register-area__content-area__container__terms-area__msg-box" for="terms"> <label class="register-area__content-area__container__checkbox-area__msg-box" for="terms">
<p class="register-area__content-area__container__terms-area__msg-box__msg"> <p class="register-area__content-area__container__checkbox-area__msg-box__msg">
I agree to the I agree to the
<a class="register-area__content-area__container__terms-area__msg-box__msg__link" href="https://storj.io/terms-of-service/" target="_blank" rel="noopener">Terms of Service</a> <a class="register-area__content-area__container__checkbox-area__msg-box__msg__link" href="https://storj.io/terms-of-service/" target="_blank" rel="noopener">Terms of Service</a>
and and
<a class="register-area__content-area__container__terms-area__msg-box__msg__link" href="https://storj.io/privacy-policy/" target="_blank" rel="noopener">Privacy Policy</a> <a class="register-area__content-area__container__checkbox-area__msg-box__msg__link" href="https://storj.io/privacy-policy/" target="_blank" rel="noopener">Privacy Policy</a>
</p> </p>
</label> </label>
</div> </div>

View File

@ -118,6 +118,8 @@ h1 {
border-top-right-radius: 20px; border-top-right-radius: 20px;
border-bottom-right-radius: 20px; border-bottom-right-radius: 20px;
border-left: none; border-left: none;
position: relative;
right: 1px;
} }
&__personal, &__personal,
@ -145,7 +147,7 @@ h1 {
padding: 60px 80px; padding: 60px 80px;
background-color: #fff; background-color: #fff;
border-radius: 6px; border-radius: 6px;
min-height: 675px; min-height: 655px;
&__title-area { &__title-area {
display: flex; display: flex;
@ -169,7 +171,7 @@ h1 {
} }
} }
&__terms-area { &__checkbox-area {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
@ -181,6 +183,8 @@ h1 {
color: #354049; color: #354049;
&__msg { &__msg {
position: relative;
top: 4px;
&__link { &__link {
margin: 0 4px; margin: 0 4px;
@ -219,7 +223,7 @@ h1 {
} }
&__container.professional-container { &__container.professional-container {
min-height: 901px; min-height: 991px;
} }
&__footer { &__footer {
@ -377,6 +381,16 @@ h1 {
} }
} }
@media screen and (max-width: 414px) {
.register-area {
&__logo-wrapper {
margin-top: 30px;
}
}
}
@media screen and (max-width: 414px) { @media screen and (max-width: 414px) {
.register-area { .register-area {