Self-hosted in-product purchase (#21804)

* Self-hosted admins can purchase licenses in-app when `ServiceSettings,SelfHostedPurchase` is true (the default)
* Content Security Policy enables loading assets from `js.stripe.com/v3` when `ServiceSettings.SelfHostedPurchase` is true (the default).
* Add `hosted_customer` API subpath
* Add status of SelfHostedPurchase to telemetry config report.
* Support showing admins self-hosted invoices when `ServiceSettings.SelfHostedPurchase` is true (the default)

Co-authored-by: Conor Macpherson <116016004+ConorMacpherson@users.noreply.github.com>
Этот коммит содержится в:
Nathaniel Allred
2022-12-13 13:36:18 -06:00
коммит произвёл GitHub
родитель 6fd174a95f
Коммит a8fa3f29e9
16 изменённых файлов: 551 добавлений и 29 удалений

Просмотреть файл

@@ -8558,6 +8558,72 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i
return newTeamMembersList, BuildResponse(r), nil
}
func (c *Client4) SelfHostedSignupAvailable() (*Response, error) {
r, err := c.DoAPIGet(c.hostedCustomerRoute()+"/signup_available", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) SelfHostedSignupCustomer(form *SelfHostedCustomerForm) (*Response, *SelfHostedSignupCustomerResponse, error) {
payloadBytes, err := json.Marshal(form)
if err != nil {
return nil, nil, NewAppError("SelfHostedSignupCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/customer", string(payloadBytes))
if err != nil {
return BuildResponse(r), nil, err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
response := SelfHostedSignupCustomerResponse{}
err = json.Unmarshal(data, &response)
if err != nil {
return BuildResponse(r), nil, err
}
return BuildResponse(r), &response, nil
}
func (c *Client4) SelfHostedSignupConfirm(form *SelfHostedConfirmPaymentMethodRequest) (*Response, *SelfHostedSignupConfirmClientResponse, error) {
payloadBytes, err := json.Marshal(form)
if err != nil {
return nil, nil, NewAppError("SelfHostedSignupConfirm", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/confirm", string(payloadBytes))
if err != nil {
return BuildResponse(r), nil, err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
response := SelfHostedSignupConfirmClientResponse{}
err = json.Unmarshal(data, &response)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
return BuildResponse(r), &response, nil
}
func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "")
if err != nil {

Просмотреть файл

@@ -290,17 +290,14 @@ type ProductLimits struct {
Teams *TeamsLimits `json:"teams,omitempty"`
}
type BootstrapSelfHostedSignupRequest struct {
Email string `json:"email"`
}
type BootstrapSelfHostedSignupResponse struct {
Progress string `json:"progress"`
}
type BootstrapSelfHostedSignupResponseInternal struct {
Progress string `json:"progress"`
License string `json:"license"`
// CreateSubscriptionRequest is the parameters for the API request to create a subscription.
type CreateSubscriptionRequest struct {
ProductID string `json:"product_id"`
AddOns []string `json:"add_ons"`
Seats int `json:"seats"`
Total float64 `json:"total"`
InternalPurchaseOrder string `json:"internal_purchase_order"`
DiscountID string `json:"discount_id"`
}
func (p *Product) IsYearly() bool {

Просмотреть файл

@@ -383,7 +383,7 @@ type ServiceSettings struct {
CollapsedThreads *string `access:"experimental_features"`
ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
EnableCustomGroups *bool `access:"site_users_and_teams"`
SelfHostedFirstTimePurchase *bool `access:"write_restrictable,cloud_restrictable"`
SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"`
AllowSyncedDrafts *bool `access:"site_posts"`
}
@@ -854,8 +854,8 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
s.AllowSyncedDrafts = NewBool(true)
}
if s.SelfHostedFirstTimePurchase == nil {
s.SelfHostedFirstTimePurchase = NewBool(false)
if s.SelfHostedPurchase == nil {
s.SelfHostedPurchase = NewBool(true)
}
}

58
model/hosted_customer.go Обычный файл
Просмотреть файл

@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type BootstrapSelfHostedSignupRequest struct {
Email string `json:"email"`
Reset bool `json:"reset"`
}
type BootstrapSelfHostedSignupResponse struct {
Progress string `json:"progress"`
}
type BootstrapSelfHostedSignupResponseInternal struct {
Progress string `json:"progress"`
License string `json:"license"`
}
// email contained in token, so not in the request body.
type SelfHostedCustomerForm struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
BillingAddress *Address `json:"billing_address"`
Organization string `json:"organization"`
}
type SelfHostedConfirmPaymentMethodRequest struct {
StripeSetupIntentID string `json:"stripe_setup_intent_id"`
Subscription CreateSubscriptionRequest `json:"subscription"`
}
// SelfHostedSignupPaymentResponse contains feels needed for self hosted signup to confirm payment and receive license.
type SelfHostedSignupCustomerResponse struct {
CustomerId string `json:"customer_id"`
SetupIntentId string `json:"setup_intent_id"`
SetupIntentSecret string `json:"setup_intent_secret"`
Progress string `json:"progress"`
}
// SelfHostedSignupConfirmResponse contains data received on successful self hosted signup
type SelfHostedSignupConfirmResponse struct {
License string `json:"license"`
Progress string `json:"progress"`
}
type SelfHostedSignupConfirmClientResponse struct {
License map[string]string `json:"license"`
Progress string `json:"progress"`
}
type SelfHostedBillingAccessRequest struct {
LicenseId string `json:"license_id"`
}
type SelfHostedBillingAccessResponse struct {
Token string `json:"token"`
}

Просмотреть файл

@@ -81,6 +81,7 @@ const (
WebsocketEventDraftDeleted = "draft_deleted"
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
WebsocketEventHostedCustomerSignupProgressUpdated = "hosted_customer_signup_progress_updated"
)
type WebSocketMessage interface {