mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: support subscription periods (yearly/monthly) (#2265)
* feat: support year/month subscription * feat: add GetPrice() for plan * feat: add GetDuration * feat: gofumpt * feat: add subscription mode for pricing * feat: restrict auto create product operation * fix: format code * feat: add period for plan,remove period from pricing * feat: format code * feat: remove space * feat: remove period in signup page
This commit is contained in:
parent
943cc43427
commit
953be4a7b6
@ -114,7 +114,7 @@ func (c *ApiController) GetPlan() {
|
|||||||
// @router /update-plan [post]
|
// @router /update-plan [post]
|
||||||
func (c *ApiController) UpdatePlan() {
|
func (c *ApiController) UpdatePlan() {
|
||||||
id := c.Input().Get("id")
|
id := c.Input().Get("id")
|
||||||
|
owner := util.GetOwnerFromId(id)
|
||||||
var plan object.Plan
|
var plan object.Plan
|
||||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -122,19 +122,21 @@ func (c *ApiController) UpdatePlan() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if plan.Product != "" {
|
if plan.Product != "" {
|
||||||
planId := util.GetId(plan.Owner, plan.Product)
|
productId := util.GetId(owner, plan.Product)
|
||||||
product, err := object.GetProduct(planId)
|
product, err := object.GetProduct(productId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if product != nil {
|
||||||
object.UpdateProductForPlan(&plan, product)
|
object.UpdateProductForPlan(&plan, product)
|
||||||
_, err = object.UpdateProduct(planId, product)
|
_, err = object.UpdateProduct(productId, product)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/xorm-io/core"
|
"github.com/xorm-io/core"
|
||||||
@ -28,10 +29,10 @@ 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"`
|
Price float64 `json:"price"`
|
||||||
PricePerYear float64 `json:"pricePerYear"`
|
|
||||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||||
Product string `json:"product"` // related product id
|
Period string `xorm:"varchar(100)" json:"period"`
|
||||||
|
Product string `xorm:"varchar(100)" json:"product"`
|
||||||
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
|
||||||
@ -39,10 +40,28 @@ type Plan struct {
|
|||||||
Options []string `xorm:"-" json:"options"`
|
Options []string `xorm:"-" json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PeriodMonthly = "Monthly"
|
||||||
|
PeriodYearly = "Yearly"
|
||||||
|
)
|
||||||
|
|
||||||
func (plan *Plan) GetId() string {
|
func (plan *Plan) GetId() string {
|
||||||
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
|
||||||
|
if period == PeriodYearly {
|
||||||
|
startTime = time.Now()
|
||||||
|
endTime = startTime.AddDate(1, 0, 0)
|
||||||
|
} else if period == PeriodMonthly {
|
||||||
|
startTime = time.Now()
|
||||||
|
endTime = startTime.AddDate(0, 1, 0)
|
||||||
|
} else {
|
||||||
|
panic(fmt.Sprintf("invalid period: %s", period))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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{})
|
||||||
|
@ -32,12 +32,6 @@ type Pricing struct {
|
|||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
TrialDuration int `json:"trialDuration"`
|
TrialDuration int `json:"trialDuration"`
|
||||||
Application string `xorm:"varchar(100)" json:"application"`
|
Application string `xorm:"varchar(100)" json:"application"`
|
||||||
|
|
||||||
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 (pricing *Pricing) GetId() string {
|
func (pricing *Pricing) GetId() string {
|
||||||
|
@ -190,8 +190,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
if user.Type == "paid-user" {
|
if user.Type == "paid-user" {
|
||||||
// Create a subscription for `paid-user`
|
// Create a subscription for `paid-user`
|
||||||
if pricingName != "" && planName != "" {
|
if pricingName != "" && planName != "" {
|
||||||
sub := NewSubscription(owner, user.Name, pricingName, planName, paymentName)
|
plan, err := GetPlan(util.GetId(owner, planName))
|
||||||
_, err := AddSubscription(sub)
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
return "", "", fmt.Errorf("the plan: %s does not exist", planName)
|
||||||
|
}
|
||||||
|
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||||
|
_, err = AddSubscription(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -267,14 +274,14 @@ func CreateProductForPlan(plan *Plan) *Product {
|
|||||||
product := &Product{
|
product := &Product{
|
||||||
Owner: plan.Owner,
|
Owner: plan.Owner,
|
||||||
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
|
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
|
||||||
DisplayName: fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName),
|
DisplayName: fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period),
|
||||||
CreatedTime: plan.CreatedTime,
|
CreatedTime: plan.CreatedTime,
|
||||||
|
|
||||||
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
|
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),
|
Detail: fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period),
|
||||||
Description: plan.Description,
|
Description: plan.Description,
|
||||||
Tag: "auto_created_product_for_plan",
|
Tag: "auto_created_product_for_plan",
|
||||||
Price: plan.PricePerMonth, // TODO
|
Price: plan.Price,
|
||||||
Currency: plan.Currency,
|
Currency: plan.Currency,
|
||||||
|
|
||||||
Quantity: 999,
|
Quantity: 999,
|
||||||
@ -290,9 +297,10 @@ func CreateProductForPlan(plan *Plan) *Product {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateProductForPlan(plan *Plan, product *Product) {
|
func UpdateProductForPlan(plan *Plan, product *Product) {
|
||||||
product.DisplayName = fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
product.Owner = plan.Owner
|
||||||
product.Detail = fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
|
||||||
product.Price = plan.PricePerMonth // TODO
|
product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
|
||||||
product.Providers = plan.PaymentProviders
|
product.Price = plan.Price
|
||||||
product.Currency = plan.Currency
|
product.Currency = plan.Currency
|
||||||
|
product.Providers = plan.PaymentProviders
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ type Subscription struct {
|
|||||||
|
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
Duration int `json:"duration"`
|
Period string `xorm:"varchar(100)" json:"period"`
|
||||||
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +103,8 @@ func (sub *Subscription) UpdateState() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubscription(owner, userName, pricingName, planName, paymentName string) *Subscription {
|
func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
|
||||||
|
startTime, endTime := GetDuration(period)
|
||||||
id := util.GenerateId()[:6]
|
id := util.GenerateId()[:6]
|
||||||
return &Subscription{
|
return &Subscription{
|
||||||
Owner: owner,
|
Owner: owner,
|
||||||
@ -112,13 +113,12 @@ func NewSubscription(owner, userName, pricingName, planName, paymentName string)
|
|||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
|
|
||||||
User: userName,
|
User: userName,
|
||||||
Pricing: pricingName,
|
|
||||||
Plan: planName,
|
Plan: planName,
|
||||||
Payment: paymentName,
|
Payment: paymentName,
|
||||||
|
|
||||||
StartTime: time.Now(),
|
StartTime: startTime,
|
||||||
EndTime: time.Now().AddDate(0, 0, 30),
|
EndTime: endTime,
|
||||||
Duration: 30, // TODO
|
Period: period,
|
||||||
State: SubStatePending, // waiting for payment complete
|
State: SubStatePending, // waiting for payment complete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,22 +197,29 @@ class PlanEditPage 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("plan:Price per month"), i18next.t("plan:Price per month - Tooltip"))} :
|
{Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
|
<InputNumber value={this.state.plan.price} onChange={value => {
|
||||||
this.updatePlanField("pricePerMonth", value);
|
this.updatePlanField("price", 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("plan:Price per year"), i18next.t("plan:Price per year - Tooltip"))} :
|
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
|
<Select
|
||||||
this.updatePlanField("pricePerYear", value);
|
defaultValue={this.state.plan.period === "" ? "Monthly" : this.state.plan.period}
|
||||||
}} />
|
onChange={value => {
|
||||||
|
this.updatePlanField("period", value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{value: "Monthly", label: "Monthly"},
|
||||||
|
{value: "Yearly", label: "Yearly"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
@ -32,9 +32,9 @@ class PlanListPage extends BaseListPage {
|
|||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
displayName: `New Plan - ${randomName}`,
|
displayName: `New Plan - ${randomName}`,
|
||||||
description: "",
|
description: "",
|
||||||
pricePerMonth: 10,
|
price: 10,
|
||||||
pricePerYear: 100,
|
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
|
period: "Monthly",
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
paymentProviders: [],
|
paymentProviders: [],
|
||||||
role: "",
|
role: "",
|
||||||
@ -136,18 +136,18 @@ class PlanListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("currency"),
|
...this.getColumnSearchProps("currency"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("plan:Price per month"),
|
title: i18next.t("plan:Price"),
|
||||||
dataIndex: "pricePerMonth",
|
dataIndex: "price",
|
||||||
key: "pricePerMonth",
|
key: "price",
|
||||||
width: "130px",
|
width: "130px",
|
||||||
...this.getColumnSearchProps("pricePerMonth"),
|
...this.getColumnSearchProps("price"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("plan:Price per year"),
|
title: i18next.t("plan:Period"),
|
||||||
dataIndex: "pricePerYear",
|
dataIndex: "period",
|
||||||
key: "pricePerYear",
|
key: "period",
|
||||||
width: "130px",
|
width: "130px",
|
||||||
...this.getColumnSearchProps("pricePerYear"),
|
...this.getColumnSearchProps("period"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Role"),
|
title: i18next.t("general:Role"),
|
||||||
|
@ -76,11 +76,10 @@ class ProductBuyPage extends React.Component {
|
|||||||
throw new Error(res.msg);
|
throw new Error(res.msg);
|
||||||
}
|
}
|
||||||
const plan = res.data;
|
const plan = res.data;
|
||||||
const productName = plan.product;
|
|
||||||
await this.setStateAsync({
|
await this.setStateAsync({
|
||||||
pricing: pricing,
|
pricing: pricing,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
productName: productName,
|
productName: plan.product,
|
||||||
});
|
});
|
||||||
this.onUpdatePricing(pricing);
|
this.onUpdatePricing(pricing);
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderProduct() {
|
renderProduct() {
|
||||||
|
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
|
||||||
return (
|
return (
|
||||||
<Card size="small" title={
|
<Card size="small" title={
|
||||||
<div>
|
<div>
|
||||||
@ -112,7 +113,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
|
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
|
||||||
{
|
{
|
||||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||||
}
|
}
|
||||||
@ -124,7 +125,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.product.name} onChange={e => {
|
<Input value={this.state.product.name} disabled={isCreatedByPlan} onChange={e => {
|
||||||
this.updateProductField("name", e.target.value);
|
this.updateProductField("name", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -171,7 +172,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.product.tag} onChange={e => {
|
<Input value={this.state.product.tag} disabled={isCreatedByPlan} onChange={e => {
|
||||||
this.updateProductField("tag", e.target.value);
|
this.updateProductField("tag", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} onChange={(value => {
|
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} disabled={isCreatedByPlan} onChange={(value => {
|
||||||
this.updateProductField("currency", value);
|
this.updateProductField("currency", value);
|
||||||
})}>
|
})}>
|
||||||
{
|
{
|
||||||
@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.price} onChange={value => {
|
<InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("price", value);
|
this.updateProductField("price", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.quantity} onChange={value => {
|
<InputNumber value={this.state.product.quantity} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("quantity", value);
|
this.updateProductField("quantity", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.sold} onChange={value => {
|
<InputNumber value={this.state.product.sold} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("sold", value);
|
this.updateProductField("sold", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
|
<Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={isCreatedByPlan} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
|
||||||
{
|
{
|
||||||
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
|
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
|
||||||
}
|
}
|
||||||
|
@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
|
|||||||
width: "230px",
|
width: "230px",
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
|
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||||
<PopconfirmModal
|
<PopconfirmModal
|
||||||
|
disabled={isCreatedByPlan}
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
onConfirm={() => this.deleteProduct(index)}
|
onConfirm={() => this.deleteProduct(index)}
|
||||||
>
|
>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
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} from "antd";
|
import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
import * as PricingBackend from "./backend/PricingBackend";
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
import * as PlanBackend from "./backend/PlanBackend";
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
@ -171,16 +171,6 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))}
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<InputNumber value={this.state.subscription.duration} onChange={value => {
|
|
||||||
this.updateSubscriptionField("duration", 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("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
||||||
@ -201,6 +191,23 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select
|
||||||
|
defaultValue={this.state.subscription.period === "" ? "Monthly" : this.state.subscription.period}
|
||||||
|
onChange={value => {
|
||||||
|
this.updateSubscriptionField("period", value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{value: "Monthly", label: "Monthly"},
|
||||||
|
{value: "Yearly", label: "Yearly"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</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:User"), i18next.t("general:User - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
||||||
|
@ -27,7 +27,6 @@ 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 = 30;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
@ -35,8 +34,8 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
displayName: `New Subscription - ${randomName}`,
|
displayName: `New Subscription - ${randomName}`,
|
||||||
startTime: moment().format(),
|
startTime: moment().format(),
|
||||||
endTime: moment().add(defaultDuration, "d").format(),
|
endTime: moment().add(30, "d").format(),
|
||||||
duration: defaultDuration,
|
period: "Monthly",
|
||||||
description: "",
|
description: "",
|
||||||
user: "",
|
user: "",
|
||||||
plan: "",
|
plan: "",
|
||||||
@ -130,11 +129,11 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("displayName"),
|
...this.getColumnSearchProps("displayName"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("subscription:Duration"),
|
title: i18next.t("subscription:Period"),
|
||||||
dataIndex: "duration",
|
dataIndex: "period",
|
||||||
key: "duration",
|
key: "period",
|
||||||
width: "140px",
|
width: "140px",
|
||||||
...this.getColumnSearchProps("duration"),
|
...this.getColumnSearchProps("period"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("subscription:Start time"),
|
title: i18next.t("subscription:Start time"),
|
||||||
@ -150,20 +149,6 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
width: "140px",
|
width: "140px",
|
||||||
...this.getColumnSearchProps("endTime"),
|
...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",
|
||||||
|
@ -179,7 +179,6 @@ class SignupPage extends React.Component {
|
|||||||
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") {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Card, Col, Row} from "antd";
|
import {Card, Col, Radio, Row} from "antd";
|
||||||
import * as PricingBackend from "../backend/PricingBackend";
|
import * as PricingBackend from "../backend/PricingBackend";
|
||||||
import * as PlanBackend from "../backend/PlanBackend";
|
import * as PlanBackend from "../backend/PlanBackend";
|
||||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||||
@ -33,6 +33,8 @@ class PricingPage extends React.Component {
|
|||||||
userName: params.get("user"),
|
userName: params.get("user"),
|
||||||
pricing: props.pricing,
|
pricing: props.pricing,
|
||||||
plans: null,
|
plans: null,
|
||||||
|
periods: null,
|
||||||
|
selectedPeriod: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -73,8 +75,12 @@ class PricingPage extends React.Component {
|
|||||||
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const plans = results.map(result => result.data);
|
||||||
|
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
|
||||||
this.setState({
|
this.setState({
|
||||||
plans: results.map(result => result.data),
|
plans: plans,
|
||||||
|
periods: periods,
|
||||||
|
selectedPeriod: periods?.[0],
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -84,10 +90,9 @@ class PricingPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadPricing(pricingName) {
|
loadPricing(pricingName) {
|
||||||
if (pricingName === undefined) {
|
if (!pricingName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PricingBackend.getPricing(this.state.owner, pricingName)
|
PricingBackend.getPricing(this.state.owner, pricingName)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
@ -106,8 +111,31 @@ class PricingPage extends React.Component {
|
|||||||
this.props.onUpdatePricing(pricing);
|
this.props.onUpdatePricing(pricing);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCards() {
|
renderSelectPeriod() {
|
||||||
|
if (!this.state.periods || this.state.periods.length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
value={this.state.selectedPeriod}
|
||||||
|
size="large"
|
||||||
|
buttonStyle="solid"
|
||||||
|
onChange={e => {
|
||||||
|
this.setState({selectedPeriod: e.target.value});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
this.state.periods.map(period => {
|
||||||
|
return (
|
||||||
|
<Radio.Button key={period} value={period}>{period}</Radio.Button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCards() {
|
||||||
const getUrlByPlan = (planName) => {
|
const getUrlByPlan = (planName) => {
|
||||||
const pricing = this.state.pricing;
|
const pricing = this.state.pricing;
|
||||||
let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
|
let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
|
||||||
@ -122,9 +150,9 @@ class PricingPage extends React.Component {
|
|||||||
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
||||||
{
|
{
|
||||||
this.state.plans.map(item => {
|
this.state.plans.map(item => {
|
||||||
return (
|
return item.period === this.state.selectedPeriod ? (
|
||||||
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||||
);
|
) : null;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Card>
|
</Card>
|
||||||
@ -135,9 +163,9 @@ class PricingPage extends React.Component {
|
|||||||
<Row style={{justifyContent: "center"}} gutter={24}>
|
<Row style={{justifyContent: "center"}} gutter={24}>
|
||||||
{
|
{
|
||||||
this.state.plans.map(item => {
|
this.state.plans.map(item => {
|
||||||
return (
|
return item.period === this.state.selectedPeriod ? (
|
||||||
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||||
);
|
) : null;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Row>
|
</Row>
|
||||||
@ -161,6 +189,13 @@ class PricingPage extends React.Component {
|
|||||||
<div className="login-form">
|
<div className="login-form">
|
||||||
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
||||||
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
||||||
|
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||||
|
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||||
|
{
|
||||||
|
this.renderSelectPeriod()
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{width: "100%", marginTop: "40px"}}>
|
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col} from "antd";
|
import {Button, Card, Col, Row} from "antd";
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import {withRouter} from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
|
|
||||||
@ -37,17 +37,21 @@ class SingleCard extends React.Component {
|
|||||||
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>}
|
title={<h2>{plan.displayName}</h2>}
|
||||||
>
|
>
|
||||||
|
<Col>
|
||||||
|
<Row>
|
||||||
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
||||||
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.pricePerMonth}</span>
|
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.price}</span>
|
||||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {plan.period === "Yearly" ? i18next.t("plan:per year") : i18next.t("plan:per month")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<br />
|
<Row style={{height: "90px", paddingTop: "15px"}}>
|
||||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||||
<Meta description={plan.description} />
|
<Meta description={plan.description} />
|
||||||
</div>
|
</div>
|
||||||
<br />
|
</Row>
|
||||||
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
|
||||||
|
{/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||||
{(plan.options ?? []).map((option) => {
|
{(plan.options ?? []).map((option) => {
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
return <li>
|
return <li>
|
||||||
@ -58,15 +62,16 @@ class SingleCard extends React.Component {
|
|||||||
<span style={{fontSize: "16px"}}>{option}</span>
|
<span style={{fontSize: "16px"}}>{option}</span>
|
||||||
</li>;
|
</li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul> */}
|
||||||
<div style={{minHeight: "60px"}}>
|
|
||||||
|
|
||||||
</div>
|
<Row style={{paddingTop: "15px"}}>
|
||||||
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
<Button style={{width: "100%", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
||||||
{
|
{
|
||||||
i18next.t("pricing:Getting started")
|
i18next.t("pricing:Getting started")
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user