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:
haiwu 2023-08-30 17:13:45 +08:00 committed by GitHub
parent 943cc43427
commit 953be4a7b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 193 additions and 130 deletions

View File

@ -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()
} }

View File

@ -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{})

View File

@ -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 {

View File

@ -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
} }

View File

@ -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
} }
} }

View File

@ -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"}} >

View File

@ -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"),

View File

@ -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);
} }

View File

@ -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>)
} }

View File

@ -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)}
> >

View File

@ -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"))} :

View File

@ -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",

View File

@ -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") {

View File

@ -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"}} >
{ {

View File

@ -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>
); );