mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-02 03:00:18 +08:00
feat: support Pricings flow (#2250)
* feat: fix price display * feat: support subscription * feat: fix select-plan-> signup -> buy-plan -> login flow * feat: support paid-user to login and jump to the pricing page * feat: support more subscription state * feat: add payment providers for plan * feat: format code * feat: gofumpt * feat: redirect to buy-plan-result page when user have pending subscription * feat: response err when pricing don't exit * Update PricingListPage.js * Update ProductBuyPage.js * Update LoginPage.js --------- Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
@ -87,6 +87,7 @@ p, *, *, GET, /api/get-prometheus-info, *, *
|
|||||||
p, *, *, *, /api/metrics, *, *
|
p, *, *, *, /api/metrics, *, *
|
||||||
p, *, *, GET, /api/get-pricing, *, *
|
p, *, *, GET, /api/get-pricing, *, *
|
||||||
p, *, *, GET, /api/get-plan, *, *
|
p, *, *, GET, /api/get-plan, *, *
|
||||||
|
p, *, *, GET, /api/get-subscription, *, *
|
||||||
p, *, *, GET, /api/get-organization-names, *, *
|
p, *, *, GET, /api/get-organization-names, *, *
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -153,12 +153,22 @@ func (c *ApiController) Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userType := "normal-user"
|
||||||
|
if authForm.Plan != "" && authForm.Pricing != "" {
|
||||||
|
err = object.CheckPricingAndPlan(authForm.Organization, authForm.Pricing, authForm.Plan)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userType = "paid-user"
|
||||||
|
}
|
||||||
|
|
||||||
user := &object.User{
|
user := &object.User{
|
||||||
Owner: authForm.Organization,
|
Owner: authForm.Organization,
|
||||||
Name: username,
|
Name: username,
|
||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
Id: id,
|
Id: id,
|
||||||
Type: "normal-user",
|
Type: userType,
|
||||||
Password: authForm.Password,
|
Password: authForm.Password,
|
||||||
DisplayName: authForm.Name,
|
DisplayName: authForm.Name,
|
||||||
Avatar: organization.DefaultAvatar,
|
Avatar: organization.DefaultAvatar,
|
||||||
@ -210,7 +220,7 @@ func (c *ApiController) Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if application.HasPromptPage() {
|
if application.HasPromptPage() && user.Type == "normal-user" {
|
||||||
// The prompt page needs the user to be signed in
|
// The prompt page needs the user to be signed in
|
||||||
c.SetSessionUsername(user.GetId())
|
c.SetSessionUsername(user.GetId())
|
||||||
}
|
}
|
||||||
@ -227,15 +237,6 @@ func (c *ApiController) Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSignupFromPricing := authForm.Plan != "" && authForm.Pricing != ""
|
|
||||||
if isSignupFromPricing {
|
|
||||||
_, err = object.Subscribe(organization.Name, user.Name, authForm.Plan, authForm.Pricing)
|
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record := object.NewRecord(c.Ctx)
|
record := object.NewRecord(c.Ctx)
|
||||||
record.Organization = application.Organization
|
record.Organization = application.Organization
|
||||||
record.User = user.Name
|
record.User = user.Name
|
||||||
|
@ -78,6 +78,46 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check whether paid-user have active subscription
|
||||||
|
if user.Type == "paid-user" {
|
||||||
|
subscriptions, err := object.GetSubscriptionsByUser(user.Owner, user.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existActiveSubscription := false
|
||||||
|
for _, subscription := range subscriptions {
|
||||||
|
if subscription.State == object.SubStateActive {
|
||||||
|
existActiveSubscription = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !existActiveSubscription {
|
||||||
|
// check pending subscription
|
||||||
|
for _, sub := range subscriptions {
|
||||||
|
if sub.State == object.SubStatePending {
|
||||||
|
c.ResponseOk("BuyPlanResult", sub)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// paid-user does not have active or pending subscription, find the default pricing of application
|
||||||
|
pricing, err := object.GetApplicationDefaultPricing(application.Organization, application.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pricing == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"), user.Name, application.Name))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// let the paid-user select plan
|
||||||
|
c.ResponseOk("SelectPlan", pricing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if form.Type == ResponseTypeLogin {
|
if form.Type == ResponseTypeLogin {
|
||||||
c.SetSessionUsername(userId)
|
c.SetSessionUsername(userId)
|
||||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||||
|
@ -16,6 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/beego/beego/utils/pagination"
|
"github.com/beego/beego/utils/pagination"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
@ -82,7 +83,10 @@ func (c *ApiController) GetPlan() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if plan == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("plan:The plan: %s does not exist"), id))
|
||||||
|
return
|
||||||
|
}
|
||||||
if includeOption {
|
if includeOption {
|
||||||
options, err := object.GetPermissionsByRole(plan.Role)
|
options, err := object.GetPermissionsByRole(plan.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -117,7 +121,20 @@ func (c *ApiController) UpdatePlan() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if plan.Product != "" {
|
||||||
|
planId := util.GetId(plan.Owner, plan.Product)
|
||||||
|
product, err := object.GetProduct(planId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
object.UpdateProductForPlan(&plan, product)
|
||||||
|
_, err = object.UpdateProduct(planId, product)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
@ -136,7 +153,14 @@ func (c *ApiController) AddPlan() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Create a related product for plan
|
||||||
|
product := object.CreateProductForPlan(&plan)
|
||||||
|
_, err = object.AddProduct(product)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plan.Product = product.Name
|
||||||
c.Data["json"] = wrapActionResponse(object.AddPlan(&plan))
|
c.Data["json"] = wrapActionResponse(object.AddPlan(&plan))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
@ -155,7 +179,13 @@ func (c *ApiController) DeletePlan() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if plan.Product != "" {
|
||||||
|
_, err = object.DeleteProduct(&object.Product{Owner: plan.Owner, Name: plan.Product})
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan))
|
c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/beego/beego/utils/pagination"
|
"github.com/beego/beego/utils/pagination"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
@ -80,7 +81,10 @@ func (c *ApiController) GetPricing() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pricing == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("pricing:The pricing: %s does not exist"), id))
|
||||||
|
return
|
||||||
|
}
|
||||||
c.ResponseOk(pricing)
|
c.ResponseOk(pricing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,10 +161,17 @@ func (c *ApiController) DeleteProduct() {
|
|||||||
// @router /buy-product [post]
|
// @router /buy-product [post]
|
||||||
func (c *ApiController) BuyProduct() {
|
func (c *ApiController) BuyProduct() {
|
||||||
id := c.Input().Get("id")
|
id := c.Input().Get("id")
|
||||||
providerName := c.Input().Get("providerName")
|
|
||||||
host := c.Ctx.Request.Host
|
host := c.Ctx.Request.Host
|
||||||
|
providerName := c.Input().Get("providerName")
|
||||||
userId := c.GetSessionUsername()
|
// buy `pricingName/planName` for `paidUserName`
|
||||||
|
pricingName := c.Input().Get("pricingName")
|
||||||
|
planName := c.Input().Get("planName")
|
||||||
|
paidUserName := c.Input().Get("userName")
|
||||||
|
owner, _ := util.GetOwnerAndNameFromId(id)
|
||||||
|
userId := util.GetId(owner, paidUserName)
|
||||||
|
if paidUserName == "" {
|
||||||
|
userId = c.GetSessionUsername()
|
||||||
|
}
|
||||||
if userId == "" {
|
if userId == "" {
|
||||||
c.ResponseError(c.T("general:Please login first"))
|
c.ResponseError(c.T("general:Please login first"))
|
||||||
return
|
return
|
||||||
@ -175,13 +182,12 @@ func (c *ApiController) BuyProduct() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
payUrl, orderId, err := object.BuyProduct(id, providerName, user, host)
|
payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -28,15 +28,21 @@ type Plan struct {
|
|||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
Description string `xorm:"varchar(100)" json:"description"`
|
Description string `xorm:"varchar(100)" json:"description"`
|
||||||
|
|
||||||
PricePerMonth float64 `json:"pricePerMonth"`
|
PricePerMonth float64 `json:"pricePerMonth"`
|
||||||
PricePerYear float64 `json:"pricePerYear"`
|
PricePerYear float64 `json:"pricePerYear"`
|
||||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
Product string `json:"product"` // related product id
|
||||||
|
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
||||||
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
|
||||||
Role string `xorm:"varchar(100)" json:"role"`
|
Role string `xorm:"varchar(100)" json:"role"`
|
||||||
Options []string `xorm:"-" json:"options"`
|
Options []string `xorm:"-" json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (plan *Plan) GetId() string {
|
||||||
|
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
||||||
|
}
|
||||||
|
|
||||||
func GetPlanCount(owner, field, value string) (int64, error) {
|
func GetPlanCount(owner, field, value string) (int64, error) {
|
||||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||||
return session.Count(&Plan{})
|
return session.Count(&Plan{})
|
||||||
@ -114,38 +120,3 @@ func DeletePlan(plan *Plan) (bool, error) {
|
|||||||
}
|
}
|
||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plan *Plan) GetId() string {
|
|
||||||
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Subscribe(owner string, user string, plan string, pricing string) (*Subscription, error) {
|
|
||||||
selectedPricing, err := GetPricing(fmt.Sprintf("%s/%s", owner, pricing))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
valid := selectedPricing != nil && selectedPricing.IsEnabled
|
|
||||||
|
|
||||||
if !valid {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
planBelongToPricing, err := selectedPricing.HasPlan(owner, plan)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if planBelongToPricing {
|
|
||||||
newSubscription := NewSubscription(owner, user, plan, selectedPricing.TrialDuration)
|
|
||||||
affected, err := AddSubscription(newSubscription)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if affected {
|
|
||||||
return newSubscription, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
@ -16,7 +16,6 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/xorm-io/core"
|
"github.com/xorm-io/core"
|
||||||
@ -41,6 +40,26 @@ type Pricing struct {
|
|||||||
State string `xorm:"varchar(100)" json:"state"`
|
State string `xorm:"varchar(100)" json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pricing *Pricing) GetId() string {
|
||||||
|
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pricing *Pricing) HasPlan(planName string) (bool, error) {
|
||||||
|
planId := util.GetId(pricing.Owner, planName)
|
||||||
|
plan, err := GetPlan(planId)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
return false, fmt.Errorf("plan: %s does not exist", planId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.InSlice(pricing.Plans, plan.Name) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetPricingCount(owner, field, value string) (int64, error) {
|
func GetPricingCount(owner, field, value string) (int64, error) {
|
||||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||||
return session.Count(&Pricing{})
|
return session.Count(&Pricing{})
|
||||||
@ -74,7 +93,7 @@ func getPricing(owner, name string) (*Pricing, error) {
|
|||||||
pricing := Pricing{Owner: owner, Name: name}
|
pricing := Pricing{Owner: owner, Name: name}
|
||||||
existed, err := ormer.Engine.Get(&pricing)
|
existed, err := ormer.Engine.Get(&pricing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &pricing, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if existed {
|
if existed {
|
||||||
return &pricing, nil
|
return &pricing, nil
|
||||||
@ -88,6 +107,20 @@ func GetPricing(id string) (*Pricing, error) {
|
|||||||
return getPricing(owner, name)
|
return getPricing(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetApplicationDefaultPricing(owner, appName string) (*Pricing, error) {
|
||||||
|
pricings := make([]*Pricing, 0, 1)
|
||||||
|
err := ormer.Engine.Asc("created_time").Find(&pricings, &Pricing{Owner: owner, Application: appName})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, pricing := range pricings {
|
||||||
|
if pricing.IsEnabled {
|
||||||
|
return pricing, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func UpdatePricing(id string, pricing *Pricing) (bool, error) {
|
func UpdatePricing(id string, pricing *Pricing) (bool, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
if p, err := getPricing(owner, name); err != nil {
|
if p, err := getPricing(owner, name); err != nil {
|
||||||
@ -120,28 +153,21 @@ func DeletePricing(pricing *Pricing) (bool, error) {
|
|||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pricing *Pricing) GetId() string {
|
func CheckPricingAndPlan(owner, pricingName, planName string) error {
|
||||||
return fmt.Sprintf("%s/%s", pricing.Owner, pricing.Name)
|
pricingId := util.GetId(owner, pricingName)
|
||||||
}
|
pricing, err := GetPricing(pricingId)
|
||||||
|
if pricing == nil || err != nil {
|
||||||
func (pricing *Pricing) HasPlan(owner string, plan string) (bool, error) {
|
if pricing == nil && err == nil {
|
||||||
selectedPlan, err := GetPlan(fmt.Sprintf("%s/%s", owner, plan))
|
err = fmt.Errorf("pricing: %s does not exist", pricingName)
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedPlan == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := false
|
|
||||||
|
|
||||||
for _, pricingPlan := range pricing.Plans {
|
|
||||||
if strings.Contains(pricingPlan, selectedPlan.Name) {
|
|
||||||
result = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
ok, err := pricing.HasPlan(planName)
|
||||||
return result, nil
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("pricing: %s does not have plan: %s", pricingName, planName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -141,24 +141,24 @@ func (product *Product) isValidProvider(provider *Provider) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (product *Product) getProvider(providerId string) (*Provider, error) {
|
func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||||
provider, err := getProvider(product.Owner, providerId)
|
provider, err := getProvider(product.Owner, providerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
return nil, fmt.Errorf("the payment provider: %s does not exist", providerId)
|
return nil, fmt.Errorf("the payment provider: %s does not exist", providerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !product.isValidProvider(provider) {
|
if !product.isValidProvider(provider) {
|
||||||
return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerId, product.Name)
|
return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerName, product.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuyProduct(id string, providerName string, user *User, host string) (string, string, error) {
|
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (string, string, error) {
|
||||||
product, err := GetProduct(id)
|
product, err := GetProduct(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@ -181,13 +181,24 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
|||||||
owner := product.Owner
|
owner := product.Owner
|
||||||
productName := product.Name
|
productName := product.Name
|
||||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||||
paymentName := util.GenerateTimeId()
|
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
||||||
productDisplayName := product.DisplayName
|
productDisplayName := product.DisplayName
|
||||||
|
|
||||||
originFrontend, originBackend := getOriginFromHost(host)
|
originFrontend, originBackend := getOriginFromHost(host)
|
||||||
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
||||||
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
||||||
// Create an Order and get the payUrl
|
if user.Type == "paid-user" {
|
||||||
|
// Create a subscription for `paid-user`
|
||||||
|
if pricingName != "" && planName != "" {
|
||||||
|
sub := NewSubscription(owner, user.Name, pricingName, planName, paymentName)
|
||||||
|
_, err := AddSubscription(sub)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create an OrderId and get the payUrl
|
||||||
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
|
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@ -228,7 +239,6 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
|||||||
if !affected {
|
if !affected {
|
||||||
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||||
}
|
}
|
||||||
|
|
||||||
return payUrl, orderId, err
|
return payUrl, orderId, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,3 +262,34 @@ func ExtendProductWithProviders(product *Product) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateProductForPlan(plan *Plan) *Product {
|
||||||
|
product := &Product{
|
||||||
|
Owner: plan.Owner,
|
||||||
|
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
|
||||||
|
DisplayName: fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName),
|
||||||
|
CreatedTime: plan.CreatedTime,
|
||||||
|
|
||||||
|
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
|
||||||
|
Detail: fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName),
|
||||||
|
Description: plan.Description,
|
||||||
|
Tag: "auto_created_product_for_plan",
|
||||||
|
Price: plan.PricePerMonth, // TODO
|
||||||
|
Currency: plan.Currency,
|
||||||
|
|
||||||
|
Quantity: 999,
|
||||||
|
Sold: 0,
|
||||||
|
|
||||||
|
Providers: plan.PaymentProviders,
|
||||||
|
State: "Published",
|
||||||
|
}
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateProductForPlan(plan *Plan, product *Product) {
|
||||||
|
product.DisplayName = fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
||||||
|
product.Detail = fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
||||||
|
product.Price = plan.PricePerMonth // TODO
|
||||||
|
product.Providers = plan.PaymentProviders
|
||||||
|
product.Currency = plan.Currency
|
||||||
|
}
|
||||||
|
@ -18,47 +18,108 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/pp"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/xorm-io/core"
|
"github.com/xorm-io/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultStatus = "Pending"
|
type SubscriptionState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubStatePending SubscriptionState = "Pending"
|
||||||
|
SubStateError SubscriptionState = "Error"
|
||||||
|
SubStateSuspended SubscriptionState = "Suspended" // suspended by the admin
|
||||||
|
|
||||||
|
SubStateActive SubscriptionState = "Active"
|
||||||
|
SubStateUpcoming SubscriptionState = "Upcoming"
|
||||||
|
SubStateExpired SubscriptionState = "Expired"
|
||||||
|
)
|
||||||
|
|
||||||
type Subscription struct {
|
type Subscription struct {
|
||||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
|
||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
|
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||||
|
Description string `xorm:"varchar(100)" json:"description"`
|
||||||
|
|
||||||
StartDate time.Time `json:"startDate"`
|
User string `xorm:"varchar(100)" json:"user"`
|
||||||
EndDate time.Time `json:"endDate"`
|
Pricing string `xorm:"varchar(100)" json:"pricing"`
|
||||||
Duration int `json:"duration"`
|
Plan string `xorm:"varchar(100)" json:"plan"`
|
||||||
Description string `xorm:"varchar(100)" json:"description"`
|
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||||
|
|
||||||
User string `xorm:"mediumtext" json:"user"`
|
StartTime time.Time `json:"startTime"`
|
||||||
Plan string `xorm:"varchar(100)" json:"plan"`
|
EndTime time.Time `json:"endTime"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
||||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
|
||||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
|
||||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
|
||||||
|
|
||||||
State string `xorm:"varchar(100)" json:"state"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubscription(owner string, user string, plan string, duration int) *Subscription {
|
func (sub *Subscription) GetId() string {
|
||||||
|
return fmt.Sprintf("%s/%s", sub.Owner, sub.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sub *Subscription) UpdateState() error {
|
||||||
|
preState := sub.State
|
||||||
|
// update subscription state by payment state
|
||||||
|
if sub.State == SubStatePending {
|
||||||
|
if sub.Payment == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payment, err := GetPayment(util.GetId(sub.Owner, sub.Payment))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if payment == nil {
|
||||||
|
sub.Description = fmt.Sprintf("payment: %s does not exist", sub.Payment)
|
||||||
|
sub.State = SubStateError
|
||||||
|
} else {
|
||||||
|
if payment.State == pp.PaymentStatePaid {
|
||||||
|
sub.State = SubStateActive
|
||||||
|
} else if payment.State != pp.PaymentStateCreated {
|
||||||
|
// other states: Canceled, Timeout, Error
|
||||||
|
sub.Description = fmt.Sprintf("payment: %s state is %v", sub.Payment, payment.State)
|
||||||
|
sub.State = SubStateError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStateExpired {
|
||||||
|
if sub.EndTime.Before(time.Now()) {
|
||||||
|
sub.State = SubStateExpired
|
||||||
|
} else if sub.StartTime.After(time.Now()) {
|
||||||
|
sub.State = SubStateUpcoming
|
||||||
|
} else {
|
||||||
|
sub.State = SubStateActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preState != sub.State {
|
||||||
|
_, err := UpdateSubscription(sub.GetId(), sub)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubscription(owner, userName, pricingName, planName, paymentName string) *Subscription {
|
||||||
id := util.GenerateId()[:6]
|
id := util.GenerateId()[:6]
|
||||||
return &Subscription{
|
return &Subscription{
|
||||||
Name: "Subscription_" + id,
|
|
||||||
DisplayName: "New Subscription - " + id,
|
|
||||||
Owner: owner,
|
Owner: owner,
|
||||||
User: owner + "/" + user,
|
Name: "sub_" + id,
|
||||||
Plan: owner + "/" + plan,
|
DisplayName: "New Subscription - " + id,
|
||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
State: defaultStatus,
|
|
||||||
Duration: duration,
|
User: userName,
|
||||||
StartDate: time.Now(),
|
Pricing: pricingName,
|
||||||
EndDate: time.Now().AddDate(0, 0, duration),
|
Plan: planName,
|
||||||
|
Payment: paymentName,
|
||||||
|
|
||||||
|
StartTime: time.Now(),
|
||||||
|
EndTime: time.Now().AddDate(0, 0, 30),
|
||||||
|
Duration: 30, // TODO
|
||||||
|
State: SubStatePending, // waiting for payment complete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +134,28 @@ func GetSubscriptions(owner string) ([]*Subscription, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return subscriptions, err
|
return subscriptions, err
|
||||||
}
|
}
|
||||||
|
for _, sub := range subscriptions {
|
||||||
|
err = sub.UpdateState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subscriptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSubscriptionsByUser(owner, userName string) ([]*Subscription, error) {
|
||||||
|
subscriptions := []*Subscription{}
|
||||||
|
err := ormer.Engine.Desc("created_time").Find(&subscriptions, &Subscription{Owner: owner, User: userName})
|
||||||
|
if err != nil {
|
||||||
|
return subscriptions, err
|
||||||
|
}
|
||||||
|
// update subscription state
|
||||||
|
for _, sub := range subscriptions {
|
||||||
|
err = sub.UpdateState()
|
||||||
|
if err != nil {
|
||||||
|
return subscriptions, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return subscriptions, nil
|
return subscriptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +166,12 @@ func GetPaginationSubscriptions(owner string, offset, limit int, field, value, s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return subscriptions, err
|
return subscriptions, err
|
||||||
}
|
}
|
||||||
|
for _, sub := range subscriptions {
|
||||||
|
err = sub.UpdateState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return subscriptions, nil
|
return subscriptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +231,3 @@ func DeleteSubscription(subscription *Subscription) (bool, error) {
|
|||||||
|
|
||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (subscription *Subscription) GetId() string {
|
|
||||||
return fmt.Sprintf("%s/%s", subscription.Owner, subscription.Name)
|
|
||||||
}
|
|
||||||
|
@ -541,7 +541,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
columns = append(columns, "name", "email", "phone", "country_code")
|
columns = append(columns, "name", "email", "phone", "country_code", "type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.ContainsString(columns, "groups") {
|
if util.ContainsString(columns, "groups") {
|
||||||
|
@ -114,8 +114,8 @@ func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, auth
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if captureRsp.Code != paypal.Success {
|
if detailRsp.Code != paypal.Success {
|
||||||
errDetail := captureRsp.ErrorResponse.Details[0]
|
errDetail := detailRsp.ErrorResponse.Details[0]
|
||||||
switch errDetail.Issue {
|
switch errDetail.Issue {
|
||||||
case "ORDER_NOT_APPROVED":
|
case "ORDER_NOT_APPROVED":
|
||||||
notifyResult.PaymentStatus = PaymentStateCanceled
|
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||||
|
@ -657,7 +657,8 @@ class App extends Component {
|
|||||||
window.location.pathname.startsWith("/result") ||
|
window.location.pathname.startsWith("/result") ||
|
||||||
window.location.pathname.startsWith("/cas") ||
|
window.location.pathname.startsWith("/cas") ||
|
||||||
window.location.pathname.startsWith("/auto-signup") ||
|
window.location.pathname.startsWith("/auto-signup") ||
|
||||||
window.location.pathname.startsWith("/select-plan");
|
window.location.pathname.startsWith("/select-plan") ||
|
||||||
|
window.location.pathname.startsWith("/buy-plan");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
|
@ -29,6 +29,8 @@ import PromptPage from "./auth/PromptPage";
|
|||||||
import ResultPage from "./auth/ResultPage";
|
import ResultPage from "./auth/ResultPage";
|
||||||
import CasLogout from "./auth/CasLogout";
|
import CasLogout from "./auth/CasLogout";
|
||||||
import {authConfig} from "./auth/Auth";
|
import {authConfig} from "./auth/Auth";
|
||||||
|
import ProductBuyPage from "./ProductBuyPage";
|
||||||
|
import PaymentResultPage from "./PaymentResultPage";
|
||||||
|
|
||||||
class EntryPage extends React.Component {
|
class EntryPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -108,7 +110,9 @@ class EntryPage extends React.Component {
|
|||||||
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
|
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />);}} />
|
||||||
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => this.renderHomeIfLoggedIn(<PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />)} />
|
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => <PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
|
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
|
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -158,14 +158,6 @@ class PaymentListPage extends BaseListPage {
|
|||||||
return Setting.getFormattedDate(text);
|
return Setting.getFormattedDate(text);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: i18next.t("general:Display name"),
|
|
||||||
// dataIndex: 'displayName',
|
|
||||||
// key: 'displayName',
|
|
||||||
// width: '160px',
|
|
||||||
// sorter: true,
|
|
||||||
// ...this.getColumnSearchProps('displayName'),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: i18next.t("provider:Type"),
|
title: i18next.t("provider:Type"),
|
||||||
dataIndex: "type",
|
dataIndex: "type",
|
||||||
@ -187,6 +179,13 @@ class PaymentListPage extends BaseListPage {
|
|||||||
// width: '160px',
|
// width: '160px',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("productDisplayName"),
|
...this.getColumnSearchProps("productDisplayName"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/products/${record.owner}/${record.productName}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("product:Price"),
|
title: i18next.t("product:Price"),
|
||||||
|
@ -15,17 +15,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Result, Spin} from "antd";
|
import {Button, Result, Spin} from "antd";
|
||||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||||
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
|
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
class PaymentResultPage extends React.Component {
|
class PaymentResultPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
this.state = {
|
this.state = {
|
||||||
classes: props,
|
classes: props,
|
||||||
paymentName: props.match.params.paymentName,
|
owner: props.match?.params?.organizationName ?? props.match?.params?.owner ?? null,
|
||||||
organizationName: props.match.params.organizationName,
|
paymentName: props.match?.params?.paymentName ?? null,
|
||||||
|
pricingName: props.pricingName ?? props.match?.params?.pricingName ?? null,
|
||||||
|
subscriptionName: params.get("subscription"),
|
||||||
payment: null,
|
payment: null,
|
||||||
|
pricing: props.pricing ?? null,
|
||||||
|
subscription: props.subscription ?? null,
|
||||||
timeout: null,
|
timeout: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -40,28 +47,77 @@ class PaymentResultPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPayment() {
|
setStateAsync(state) {
|
||||||
PaymentBackend.getPayment(this.state.organizationName, this.state.paymentName)
|
return new Promise((resolve, reject) => {
|
||||||
.then((res) => {
|
this.setState(state, () => {
|
||||||
this.setState({
|
resolve();
|
||||||
payment: res.data,
|
|
||||||
});
|
|
||||||
// window.console.log("payment=", res.data);
|
|
||||||
if (res.data.state === "Created") {
|
|
||||||
if (["PayPal", "Stripe"].includes(res.data.type)) {
|
|
||||||
this.setState({
|
|
||||||
timeout: setTimeout(() => {
|
|
||||||
PaymentBackend.notifyPayment(this.state.organizationName, this.state.paymentName)
|
|
||||||
.then((res) => {
|
|
||||||
this.getPayment();
|
|
||||||
});
|
|
||||||
}, 1000),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({timeout: setTimeout(() => this.getPayment(), 1000)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdatePricing(pricing) {
|
||||||
|
this.props.onUpdatePricing(pricing);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPayment() {
|
||||||
|
if (!(this.state.owner && (this.state.paymentName || (this.state.pricingName && this.state.subscriptionName)))) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// loading price & subscription
|
||||||
|
if (this.state.pricingName && this.state.subscriptionName) {
|
||||||
|
if (!this.state.pricing) {
|
||||||
|
const res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const pricing = res.data;
|
||||||
|
await this.setStateAsync({
|
||||||
|
pricing: pricing,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.state.subscription) {
|
||||||
|
const res = await SubscriptionBackend.getSubscription(this.state.owner, this.state.subscriptionName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const subscription = res.data;
|
||||||
|
await this.setStateAsync({
|
||||||
|
subscription: subscription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const paymentName = this.state.subscription.payment;
|
||||||
|
await this.setStateAsync({
|
||||||
|
paymentName: paymentName,
|
||||||
|
});
|
||||||
|
this.onUpdatePricing(this.state.pricing);
|
||||||
|
}
|
||||||
|
const res = await PaymentBackend.getPayment(this.state.owner, this.state.paymentName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const payment = res.data;
|
||||||
|
await this.setStateAsync({
|
||||||
|
payment: payment,
|
||||||
|
});
|
||||||
|
if (payment.state === "Created") {
|
||||||
|
if (["PayPal", "Stripe"].includes(payment.type)) {
|
||||||
|
this.setState({
|
||||||
|
timeout: setTimeout(async() => {
|
||||||
|
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||||
|
this.getPayment();
|
||||||
|
}, 1000),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
timeout: setTimeout(() => this.getPayment(), 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Setting.showMessage("error", err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPaymentUrl(payment) {
|
goToPaymentUrl(payment) {
|
||||||
@ -81,7 +137,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
|
|
||||||
if (payment.state === "Paid") {
|
if (payment.state === "Paid") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
{
|
{
|
||||||
Setting.renderHelmet(payment)
|
Setting.renderHelmet(payment)
|
||||||
}
|
}
|
||||||
@ -101,7 +157,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
);
|
);
|
||||||
} else if (payment.state === "Created") {
|
} else if (payment.state === "Created") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
{
|
{
|
||||||
Setting.renderHelmet(payment)
|
Setting.renderHelmet(payment)
|
||||||
}
|
}
|
||||||
@ -117,7 +173,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
);
|
);
|
||||||
} else if (payment.state === "Canceled") {
|
} else if (payment.state === "Canceled") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
{
|
{
|
||||||
Setting.renderHelmet(payment)
|
Setting.renderHelmet(payment)
|
||||||
}
|
}
|
||||||
@ -137,7 +193,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
);
|
);
|
||||||
} else if (payment.state === "Timeout") {
|
} else if (payment.state === "Timeout") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
{
|
{
|
||||||
Setting.renderHelmet(payment)
|
Setting.renderHelmet(payment)
|
||||||
}
|
}
|
||||||
@ -157,7 +213,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
{
|
{
|
||||||
Setting.renderHelmet(payment)
|
Setting.renderHelmet(payment)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
|
|||||||
import * as RoleBackend from "./backend/RoleBackend";
|
import * as RoleBackend from "./backend/RoleBackend";
|
||||||
import * as PlanBackend from "./backend/PlanBackend";
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
import * as UserBackend from "./backend/UserBackend";
|
import * as UserBackend from "./backend/UserBackend";
|
||||||
|
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -28,14 +29,14 @@ class PlanEditPage extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
classes: props,
|
classes: props,
|
||||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
organizationName: props?.organizationName ?? props?.match?.params?.organizationName ?? null,
|
||||||
planName: props.match.params.planName,
|
planName: props?.match?.params?.planName ?? null,
|
||||||
plan: null,
|
plan: null,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
users: [],
|
users: [],
|
||||||
roles: [],
|
roles: [],
|
||||||
providers: [],
|
paymentProviders: [],
|
||||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
mode: props?.location?.mode ?? "edit",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ class PlanEditPage extends React.Component {
|
|||||||
|
|
||||||
this.getUsers(this.state.organizationName);
|
this.getUsers(this.state.organizationName);
|
||||||
this.getRoles(this.state.organizationName);
|
this.getRoles(this.state.organizationName);
|
||||||
|
this.getPaymentProviders(this.state.organizationName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +91,20 @@ class PlanEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPaymentProviders(organizationName) {
|
||||||
|
ProviderBackend.getProviders(organizationName)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === "ok") {
|
||||||
|
this.setState({
|
||||||
|
paymentProviders: res.data.filter(provider => provider.category === "Payment"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.showMessage("error", res.msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getOrganizations() {
|
getOrganizations() {
|
||||||
OrganizationBackend.getOrganizations("admin")
|
OrganizationBackend.getOrganizations("admin")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -165,7 +181,7 @@ class PlanEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})}
|
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} onChange={(value => {this.updatePlanField("role", value);})}
|
||||||
options={this.state.roles.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`))
|
options={this.state.roles.map((role) => Setting.getOption(role.name, role.name))
|
||||||
} />
|
} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -216,6 +232,18 @@ class PlanEditPage extends React.Component {
|
|||||||
</Select>
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.plan.paymentProviders ?? []} onChange={(value => {this.updatePlanField("paymentProviders", value);})}>
|
||||||
|
{
|
||||||
|
this.state.paymentProviders.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||||
|
@ -126,6 +126,14 @@ class PlanListPage extends BaseListPage {
|
|||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("displayName"),
|
...this.getColumnSearchProps("displayName"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("payment:Currency"),
|
||||||
|
dataIndex: "currency",
|
||||||
|
key: "currency",
|
||||||
|
width: "120px",
|
||||||
|
sorter: true,
|
||||||
|
...this.getColumnSearchProps("currency"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("plan:Price per month"),
|
title: i18next.t("plan:Price per month"),
|
||||||
dataIndex: "pricePerMonth",
|
dataIndex: "pricePerMonth",
|
||||||
@ -148,7 +156,21 @@ class PlanListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("role"),
|
...this.getColumnSearchProps("role"),
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (
|
return (
|
||||||
<Link to={`/roles/${encodeURIComponent(text)}`}>
|
<Link to={`/roles/${record.owner}/${text}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("plan:Related product"),
|
||||||
|
dataIndex: "product",
|
||||||
|
key: "product",
|
||||||
|
width: "130px",
|
||||||
|
...this.getColumnSearchProps("product"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/products/${record.owner}/${text}`}>
|
||||||
{text}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -190,7 +190,7 @@ class PricingEditPage extends React.Component {
|
|||||||
onChange={(value => {
|
onChange={(value => {
|
||||||
this.updatePricingField("plans", value);
|
this.updatePricingField("plans", value);
|
||||||
})}
|
})}
|
||||||
options={this.state.plans.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))}
|
options={this.state.plans.map((plan) => Setting.getOption(plan.name, plan.name))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -294,7 +294,7 @@ class PricingEditPage extends React.Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<PricingPage pricing={this.state.pricing}></PricingPage>
|
<PricingPage pricing={this.state.pricing} owner={this.state.pricing.owner}></PricingPage>
|
||||||
</Col>
|
</Col>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button, Switch, Table} from "antd";
|
import {Button, Col, Row, Switch, Table, Tooltip} from "antd";
|
||||||
|
import {EditOutlined} from "@ant-design/icons";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import * as PricingBackend from "./backend/PricingBackend";
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
@ -118,11 +119,58 @@ class PricingListPage extends BaseListPage {
|
|||||||
title: i18next.t("general:Display name"),
|
title: i18next.t("general:Display name"),
|
||||||
dataIndex: "displayName",
|
dataIndex: "displayName",
|
||||||
key: "displayName",
|
key: "displayName",
|
||||||
// width: "170px",
|
width: "170px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("displayName"),
|
...this.getColumnSearchProps("displayName"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Application"),
|
||||||
|
dataIndex: "application",
|
||||||
|
key: "application",
|
||||||
|
width: "170px",
|
||||||
|
sorter: true,
|
||||||
|
...this.getColumnSearchProps("application"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/applications/${record.owner}/${text}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Plans"),
|
||||||
|
dataIndex: "plans",
|
||||||
|
key: "plans",
|
||||||
|
// width: "170px",
|
||||||
|
sorter: true,
|
||||||
|
...this.getColumnSearchProps("plans"),
|
||||||
|
render: (plans, record, index) => {
|
||||||
|
if (plans.length === 0) {
|
||||||
|
return `(${i18next.t("general:empty")})`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
{
|
||||||
|
plans.map((plan) => (
|
||||||
|
<Col key={plan}>
|
||||||
|
<div style={{display: "inline", marginRight: "20px"}}>
|
||||||
|
<Tooltip placement="topLeft" title="Edit">
|
||||||
|
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/plans/${record.owner}/${plan}`)} />
|
||||||
|
</Tooltip>
|
||||||
|
<Link to={`/plans/${record.owner}/${plan}`}>
|
||||||
|
{plan}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Is enabled"),
|
title: i18next.t("general:Is enabled"),
|
||||||
dataIndex: "isEnabled",
|
dataIndex: "isEnabled",
|
||||||
|
@ -17,16 +17,24 @@ import {Button, Descriptions, Modal, Spin} from "antd";
|
|||||||
import {CheckCircleTwoTone} from "@ant-design/icons";
|
import {CheckCircleTwoTone} from "@ant-design/icons";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import * as ProductBackend from "./backend/ProductBackend";
|
import * as ProductBackend from "./backend/ProductBackend";
|
||||||
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
|
|
||||||
class ProductBuyPage extends React.Component {
|
class ProductBuyPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
this.state = {
|
this.state = {
|
||||||
classes: props,
|
classes: props,
|
||||||
organizationName: props.organizationName !== undefined ? props.organizationName : props?.match?.params?.organizationName,
|
owner: props?.organizationName ?? props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
|
||||||
productName: props.productName !== undefined ? props.productName : props?.match?.params?.productName,
|
productName: props?.productName ?? props?.match?.params?.productName ?? null,
|
||||||
|
pricingName: props?.pricingName ?? props?.match?.params?.pricingName ?? null,
|
||||||
|
planName: params.get("plan"),
|
||||||
|
userName: params.get("user"),
|
||||||
product: null,
|
product: null,
|
||||||
|
pricing: props?.pricing ?? null,
|
||||||
|
plan: null,
|
||||||
isPlacingOrder: false,
|
isPlacingOrder: false,
|
||||||
qrCodeModalProvider: null,
|
qrCodeModalProvider: null,
|
||||||
};
|
};
|
||||||
@ -36,20 +44,58 @@ class ProductBuyPage extends React.Component {
|
|||||||
this.getProduct();
|
this.getProduct();
|
||||||
}
|
}
|
||||||
|
|
||||||
getProduct() {
|
setStateAsync(state) {
|
||||||
if (this.state.productName === undefined || this.state.organizationName === undefined) {
|
return new Promise((resolve, reject) => {
|
||||||
|
this.setState(state, () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdatePricing(pricing) {
|
||||||
|
this.props.onUpdatePricing(pricing);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProduct() {
|
||||||
|
if (!this.state.owner || (!this.state.productName && !this.state.pricingName)) {
|
||||||
return ;
|
return ;
|
||||||
}
|
}
|
||||||
ProductBackend.getProduct(this.state.organizationName, this.state.productName)
|
try {
|
||||||
.then((res) => {
|
// load pricing & plan
|
||||||
if (res.status === "error") {
|
if (this.state.pricingName) {
|
||||||
Setting.showMessage("error", res.msg);
|
if (!this.state.planName || !this.state.userName) {
|
||||||
return;
|
return ;
|
||||||
}
|
}
|
||||||
this.setState({
|
let res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
|
||||||
product: res.data,
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const pricing = res.data;
|
||||||
|
res = await PlanBackend.getPlan(this.state.owner, this.state.planName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const plan = res.data;
|
||||||
|
const productName = plan.product;
|
||||||
|
await this.setStateAsync({
|
||||||
|
pricing: pricing,
|
||||||
|
plan: plan,
|
||||||
|
productName: productName,
|
||||||
});
|
});
|
||||||
|
this.onUpdatePricing(pricing);
|
||||||
|
}
|
||||||
|
// load product
|
||||||
|
const res = await ProductBackend.getProduct(this.state.owner, this.state.productName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
product: res.data,
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Setting.showMessage("error", err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getProductObj() {
|
getProductObj() {
|
||||||
@ -96,7 +142,7 @@ class ProductBuyPage extends React.Component {
|
|||||||
isPlacingOrder: true,
|
isPlacingOrder: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
ProductBackend.buyProduct(product.owner, product.name, provider.name)
|
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const payUrl = res.data;
|
const payUrl = res.data;
|
||||||
@ -215,11 +261,11 @@ class ProductBuyPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="login-content">
|
||||||
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
|
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
|
||||||
<Descriptions title={i18next.t("product:Buy Product")} bordered>
|
<Descriptions title={<span style={{fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
|
||||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||||
<span style={{fontSize: 28}}>
|
<span style={{fontSize: 25}}>
|
||||||
{Setting.getLanguageText(product?.displayName)}
|
{Setting.getLanguageText(product?.displayName)}
|
||||||
</span>
|
</span>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
@ -1170,9 +1170,9 @@ export function getTags(tags, urlPrefix = null) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTag(color, text) {
|
export function getTag(color, text, icon) {
|
||||||
return (
|
return (
|
||||||
<Tag color={color}>
|
<Tag color={color} icon={icon}>
|
||||||
{text}
|
{text}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@ -1254,3 +1254,13 @@ export function builtInObject(obj) {
|
|||||||
}
|
}
|
||||||
return obj.owner === "built-in" && BuiltInObjects.includes(obj.name);
|
return obj.owner === "built-in" && BuiltInObjects.includes(obj.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrencySymbol(currency) {
|
||||||
|
if (currency === "USD" || currency === "usd") {
|
||||||
|
return "$";
|
||||||
|
} else if (currency === "CNY" || currency === "cny") {
|
||||||
|
return "¥";
|
||||||
|
} else {
|
||||||
|
return currency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select, Switch} from "antd";
|
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select} from "antd";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
import * as PlanBackend from "./backend/PlanBackend";
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||||
import * as UserBackend from "./backend/UserBackend";
|
import * as UserBackend from "./backend/UserBackend";
|
||||||
@ -33,7 +34,8 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
subscription: null,
|
subscription: null,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
users: [],
|
users: [],
|
||||||
planes: [],
|
pricings: [],
|
||||||
|
plans: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||||
};
|
};
|
||||||
@ -62,15 +64,25 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.getUsers(this.state.organizationName);
|
this.getUsers(this.state.organizationName);
|
||||||
this.getPlanes(this.state.organizationName);
|
this.getPricings(this.state.organizationName);
|
||||||
|
this.getPlans(this.state.organizationName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlanes(organizationName) {
|
getPricings(organizationName) {
|
||||||
|
PricingBackend.getPricings(organizationName)
|
||||||
|
.then((res) => {
|
||||||
|
this.setState({
|
||||||
|
pricings: res.data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlans(organizationName) {
|
||||||
PlanBackend.getPlans(organizationName)
|
PlanBackend.getPlans(organizationName)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
planes: res.data,
|
plans: res.data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -133,7 +145,7 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} onChange={(owner => {
|
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} onChange={(owner => {
|
||||||
this.updateSubscriptionField("owner", owner);
|
this.updateSubscriptionField("owner", owner);
|
||||||
this.getUsers(owner);
|
this.getUsers(owner);
|
||||||
this.getPlanes(owner);
|
this.getPlans(owner);
|
||||||
})}
|
})}
|
||||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||||
} />
|
} />
|
||||||
@ -171,21 +183,21 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("subscription:Start date"), i18next.t("subscription:Start date - Tooltip"))}
|
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<DatePicker value={dayjs(this.state.subscription.startDate)} onChange={value => {
|
<DatePicker value={dayjs(this.state.subscription.startTime)} onChange={value => {
|
||||||
this.updateSubscriptionField("startDate", value);
|
this.updateSubscriptionField("startTime", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("subscription:End date"), i18next.t("subscription:End date - Tooltip"))}
|
{Setting.getLabel(i18next.t("subscription:End time"), i18next.t("subscription:End time - Tooltip"))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<DatePicker value={dayjs(this.state.subscription.endDate)} onChange={value => {
|
<DatePicker value={dayjs(this.state.subscription.endTime)} onChange={value => {
|
||||||
this.updateSubscriptionField("endDate", value);
|
this.updateSubscriptionField("endTime", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -196,21 +208,42 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select style={{width: "100%"}} value={this.state.subscription.user}
|
<Select style={{width: "100%"}} value={this.state.subscription.user}
|
||||||
onChange={(value => {this.updateSubscriptionField("user", value);})}
|
onChange={(value => {this.updateSubscriptionField("user", value);})}
|
||||||
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
|
options={this.state.users.map((user) => Setting.getOption(user.name, user.name))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Pricing"), i18next.t("general:Pricing - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.pricing}
|
||||||
|
onChange={(value => {this.updateSubscriptionField("pricing", value);})}
|
||||||
|
options={this.state.pricings.map((pricing) => Setting.getOption(pricing.name, pricing.name))
|
||||||
|
} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Plan"), i18next.t("general:Plan - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Plan"), i18next.t("general:Plan - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan} onChange={(value => {this.updateSubscriptionField("plan", value);})}
|
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.plan}
|
||||||
options={this.state.planes.map((plan) => Setting.getOption(`${plan.owner}/${plan.name}`, `${plan.owner}/${plan.name}`))
|
onChange={(value => {this.updateSubscriptionField("plan", value);})}
|
||||||
|
options={this.state.plans.map((plan) => Setting.getOption(plan.name, plan.name))
|
||||||
} />
|
} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Payment"), i18next.t("general:Payment - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input value={this.state.subscription.payment} disabled={true} onChange={e => {
|
||||||
|
this.updateSubscriptionField("payment", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} :
|
||||||
@ -221,46 +254,6 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={1} >
|
|
||||||
<Switch checked={this.state.subscription.isEnabled} onChange={checked => {
|
|
||||||
this.updateSubscriptionField("isEnabled", checked);
|
|
||||||
}} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("permission:Submitter"), i18next.t("permission:Submitter - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<Input disabled={true} value={this.state.subscription.submitter} onChange={e => {
|
|
||||||
this.updateSubscriptionField("submitter", e.target.value);
|
|
||||||
}} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("permission:Approver"), i18next.t("permission:Approver - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<Input disabled={true} value={this.state.subscription.approver} onChange={e => {
|
|
||||||
this.updateSubscriptionField("approver", e.target.value);
|
|
||||||
}} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("permission:Approve time"), i18next.t("permission:Approve time - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<Input disabled={true} value={Setting.getFormattedDate(this.state.subscription.approveTime)} onChange={e => {
|
|
||||||
this.updatePermissionField("approveTime", e.target.value);
|
|
||||||
}} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
|
||||||
@ -280,8 +273,12 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
this.updateSubscriptionField("state", value);
|
this.updateSubscriptionField("state", value);
|
||||||
})}
|
})}
|
||||||
options={[
|
options={[
|
||||||
{value: "Approved", name: i18next.t("permission:Approved")},
|
|
||||||
{value: "Pending", name: i18next.t("permission:Pending")},
|
{value: "Pending", name: i18next.t("permission:Pending")},
|
||||||
|
{value: "Active", name: i18next.t("permission:Active")},
|
||||||
|
{value: "Upcoming", name: i18next.t("permission:Upcoming")},
|
||||||
|
{value: "Expired", name: i18next.t("permission:Expired")},
|
||||||
|
{value: "Error", name: i18next.t("permission:Error")},
|
||||||
|
{value: "Suspended", name: i18next.t("permission:Suspended")},
|
||||||
].map((item) => Setting.getOption(item.name, item.value))}
|
].map((item) => Setting.getOption(item.name, item.value))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button, Table} from "antd";
|
import {Button, Table} from "antd";
|
||||||
|
import {ClockCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, MinusCircleOutlined, SyncOutlined} from "@ant-design/icons";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
import * as SubscriptionBackend from "./backend/SubscriptionBackend";
|
||||||
@ -26,24 +27,20 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
newSubscription() {
|
newSubscription() {
|
||||||
const randomName = Setting.getRandomName();
|
const randomName = Setting.getRandomName();
|
||||||
const owner = Setting.getRequestOrganization(this.props.account);
|
const owner = Setting.getRequestOrganization(this.props.account);
|
||||||
const defaultDuration = 365;
|
const defaultDuration = 30;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
name: `subscription_${randomName}`,
|
name: `sub_${randomName}`,
|
||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
displayName: `New Subscription - ${randomName}`,
|
displayName: `New Subscription - ${randomName}`,
|
||||||
startDate: moment().format(),
|
startTime: moment().format(),
|
||||||
endDate: moment().add(defaultDuration, "d").format(),
|
endTime: moment().add(defaultDuration, "d").format(),
|
||||||
duration: defaultDuration,
|
duration: defaultDuration,
|
||||||
description: "",
|
description: "",
|
||||||
user: "",
|
user: "",
|
||||||
plan: "",
|
plan: "",
|
||||||
isEnabled: true,
|
state: "Active",
|
||||||
submitter: this.props.account.name,
|
|
||||||
approver: this.props.account.name,
|
|
||||||
approveTime: moment().format(),
|
|
||||||
state: "Approved",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +136,34 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
width: "140px",
|
width: "140px",
|
||||||
...this.getColumnSearchProps("duration"),
|
...this.getColumnSearchProps("duration"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("subscription:Start time"),
|
||||||
|
dataIndex: "startTime",
|
||||||
|
key: "startTime",
|
||||||
|
width: "140px",
|
||||||
|
...this.getColumnSearchProps("startTime"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("subscription:End time"),
|
||||||
|
dataIndex: "endTime",
|
||||||
|
key: "endTime",
|
||||||
|
width: "140px",
|
||||||
|
...this.getColumnSearchProps("endTime"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Pricing"),
|
||||||
|
dataIndex: "pricing",
|
||||||
|
key: "pricing",
|
||||||
|
width: "140px",
|
||||||
|
...this.getColumnSearchProps("pricing"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/pricings/${record.owner}/${text}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Plan"),
|
title: i18next.t("general:Plan"),
|
||||||
dataIndex: "plan",
|
dataIndex: "plan",
|
||||||
@ -147,7 +172,7 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("plan"),
|
...this.getColumnSearchProps("plan"),
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (
|
return (
|
||||||
<Link to={`/plans/${text}`}>
|
<Link to={`/plans/${record.owner}/${text}`}>
|
||||||
{text}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -161,7 +186,21 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("user"),
|
...this.getColumnSearchProps("user"),
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (
|
return (
|
||||||
<Link to={`/users/${text}`}>
|
<Link to={`/users/${record.owner}/${text}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Payment"),
|
||||||
|
dataIndex: "payment",
|
||||||
|
key: "payment",
|
||||||
|
width: "140px",
|
||||||
|
...this.getColumnSearchProps("payment"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/payments/${record.owner}/${text}`}>
|
||||||
{text}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -176,10 +215,18 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("state"),
|
...this.getColumnSearchProps("state"),
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
switch (text) {
|
switch (text) {
|
||||||
case "Approved":
|
|
||||||
return Setting.getTag("success", i18next.t("permission:Approved"));
|
|
||||||
case "Pending":
|
case "Pending":
|
||||||
return Setting.getTag("error", i18next.t("permission:Pending"));
|
return Setting.getTag("processing", i18next.t("permission:Pending"), <ExclamationCircleOutlined />);
|
||||||
|
case "Active":
|
||||||
|
return Setting.getTag("success", i18next.t("permission:Active"), <SyncOutlined spin />);
|
||||||
|
case "Upcoming":
|
||||||
|
return Setting.getTag("warning", i18next.t("permission:Upcoming"), <ClockCircleOutlined />);
|
||||||
|
case "Expired":
|
||||||
|
return Setting.getTag("warning", i18next.t("permission:Expired"), <ClockCircleOutlined />);
|
||||||
|
case "Error":
|
||||||
|
return Setting.getTag("error", i18next.t("permission:Error"), <CloseCircleOutlined />);
|
||||||
|
case "Suspended":
|
||||||
|
return Setting.getTag("default", i18next.t("permission:Suspended"), <MinusCircleOutlined />);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -390,7 +390,7 @@ class UserEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})}
|
<Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})}
|
||||||
options={["normal-user"].map(item => Setting.getOption(item, item))}
|
options={["normal-user", "paid-user"].map(item => Setting.getOption(item, item))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -403,6 +403,14 @@ class LoginPage extends React.Component {
|
|||||||
/>);
|
/>);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (res.data === "SelectPlan") {
|
||||||
|
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
||||||
|
const pricing = res.data2;
|
||||||
|
Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
|
||||||
|
} else if (res.data === "BuyPlanResult") {
|
||||||
|
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
|
||||||
|
const sub = res.data2;
|
||||||
|
Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
|
||||||
} else {
|
} else {
|
||||||
callback(res);
|
callback(res);
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,11 @@ class SignupPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getResultPath(application) {
|
getResultPath(application, signupParams) {
|
||||||
|
if (signupParams?.plan && signupParams?.pricing) {
|
||||||
|
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
|
||||||
|
return `/buy-plan/${application.organization}/${signupParams?.pricing}?user=${signupParams.username}&plan=${signupParams.plan}`;
|
||||||
|
}
|
||||||
if (authConfig.appName === application.name) {
|
if (authConfig.appName === application.name) {
|
||||||
return "/result";
|
return "/result";
|
||||||
} else {
|
} else {
|
||||||
@ -173,13 +177,13 @@ class SignupPage extends React.Component {
|
|||||||
const application = this.getApplicationObj();
|
const application = this.getApplicationObj();
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
values["plan"] = params.get("plan");
|
values.plan = params.get("plan");
|
||||||
values["pricing"] = params.get("pricing");
|
values.pricing = params.get("pricing");
|
||||||
|
|
||||||
AuthBackend.signup(values)
|
AuthBackend.signup(values)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (Setting.hasPromptPage(application)) {
|
if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
|
||||||
AuthBackend.getAccount("")
|
AuthBackend.getAccount("")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
let account = null;
|
let account = null;
|
||||||
@ -188,13 +192,13 @@ class SignupPage extends React.Component {
|
|||||||
account.organization = res.data2;
|
account.organization = res.data2;
|
||||||
|
|
||||||
this.onUpdateAccount(account);
|
this.onUpdateAccount(account);
|
||||||
Setting.goToLinkSoft(this, this.getResultPath(application));
|
Setting.goToLinkSoft(this, this.getResultPath(application, values));
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Setting.goToLinkSoft(this, this.getResultPath(application));
|
Setting.goToLinkSoft(this, this.getResultPath(application, values));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
|
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
|
||||||
|
@ -24,18 +24,8 @@ export function getPlans(owner, page = "", pageSize = "", field = "", value = ""
|
|||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlanById(id, includeOption = false) {
|
export function getPlan(owner, name, includeOption = false) {
|
||||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${id}&includeOption=${includeOption}`, {
|
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}&includeOption=${includeOption}`, {
|
||||||
method: "GET",
|
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Accept-Language": Setting.getAcceptLanguage(),
|
|
||||||
},
|
|
||||||
}).then(res => res.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPlan(owner, name) {
|
|
||||||
return fetch(`${Setting.ServerUrl}/api/get-plan?id=${owner}/${encodeURIComponent(name)}`, {
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -70,8 +70,8 @@ export function deleteProduct(product) {
|
|||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buyProduct(owner, name, providerId) {
|
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "") {
|
||||||
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerId}`, {
|
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -24,11 +24,13 @@ import i18next from "i18next";
|
|||||||
class PricingPage extends React.Component {
|
class PricingPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
this.state = {
|
this.state = {
|
||||||
classes: props,
|
classes: props,
|
||||||
applications: null,
|
applications: null,
|
||||||
owner: props.owner ?? (props.match?.params?.owner ?? null),
|
owner: props.owner ?? (props.match?.params?.owner ?? null),
|
||||||
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
|
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
|
||||||
|
userName: params.get("user"),
|
||||||
pricing: props.pricing,
|
pricing: props.pricing,
|
||||||
plans: null,
|
plans: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -39,7 +41,9 @@ class PricingPage extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
applications: [],
|
applications: [],
|
||||||
});
|
});
|
||||||
|
if (this.state.userName) {
|
||||||
|
Setting.showMessage("info", `${i18next.t("pricing:paid-user do not have active subscription or pending subscription, please select a plan to buy")}`);
|
||||||
|
}
|
||||||
if (this.state.pricing) {
|
if (this.state.pricing) {
|
||||||
this.loadPlans();
|
this.loadPlans();
|
||||||
} else {
|
} else {
|
||||||
@ -60,7 +64,7 @@ class PricingPage extends React.Component {
|
|||||||
|
|
||||||
loadPlans() {
|
loadPlans() {
|
||||||
const plans = this.state.pricing.plans.map((plan) =>
|
const plans = this.state.pricing.plans.map((plan) =>
|
||||||
PlanBackend.getPlanById(plan, true));
|
PlanBackend.getPlan(this.state.owner, plan, true));
|
||||||
|
|
||||||
Promise.all(plans)
|
Promise.all(plans)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
@ -70,7 +74,7 @@ class PricingPage extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
plans: results,
|
plans: results.map(result => result.data),
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -90,7 +94,6 @@ class PricingPage extends React.Component {
|
|||||||
Setting.showMessage("error", res.msg);
|
Setting.showMessage("error", res.msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
pricing: res.data,
|
pricing: res.data,
|
||||||
@ -105,9 +108,12 @@ class PricingPage extends React.Component {
|
|||||||
|
|
||||||
renderCards() {
|
renderCards() {
|
||||||
|
|
||||||
const getUrlByPlan = (plan) => {
|
const getUrlByPlan = (planName) => {
|
||||||
const pricing = this.state.pricing;
|
const pricing = this.state.pricing;
|
||||||
const signUpUrl = `/signup/${pricing.application}?plan=${plan}&pricing=${pricing.name}`;
|
let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
|
||||||
|
if (this.state.userName) {
|
||||||
|
signUpUrl = `/buy-plan/${pricing.owner}/${pricing.name}?plan=${planName}&user=${this.state.userName}`;
|
||||||
|
}
|
||||||
return `${window.location.origin}${signUpUrl}`;
|
return `${window.location.origin}${signUpUrl}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,21 +29,16 @@ class SingleCard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCard(plan, isSingle, link) {
|
renderCard(plan, isSingle, link) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
|
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
onClick={() => Setting.isMobile() ? window.location.href = link : null}
|
onClick={() => Setting.isMobile() ? window.location.href = link : null}
|
||||||
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
||||||
|
title={<h2>{plan.displayName}</h2>}
|
||||||
>
|
>
|
||||||
<div style={{textAlign: "right"}}>
|
|
||||||
<h2
|
|
||||||
style={{marginTop: "0px"}}>{plan.displayName}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
||||||
<span style={{fontWeight: 700, fontSize: "48px"}}>$ {plan.pricePerMonth}</span>
|
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.pricePerMonth}</span>
|
||||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user