mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-22 18:25:47 +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]
|
||||
func (c *ApiController) UpdatePlan() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
owner := util.GetOwnerFromId(id)
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
if err != nil {
|
||||
@ -122,19 +122,21 @@ func (c *ApiController) UpdatePlan() {
|
||||
return
|
||||
}
|
||||
if plan.Product != "" {
|
||||
planId := util.GetId(plan.Owner, plan.Product)
|
||||
product, err := object.GetProduct(planId)
|
||||
productId := util.GetId(owner, plan.Product)
|
||||
product, err := object.GetProduct(productId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if product != nil {
|
||||
object.UpdateProductForPlan(&plan, product)
|
||||
_, err = object.UpdateProduct(planId, product)
|
||||
_, err = object.UpdateProduct(productId, product)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@ -28,10 +29,10 @@ type Plan struct {
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
|
||||
PricePerMonth float64 `json:"pricePerMonth"`
|
||||
PricePerYear float64 `json:"pricePerYear"`
|
||||
Price float64 `json:"price"`
|
||||
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
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
||||
@ -39,10 +40,28 @@ type Plan struct {
|
||||
Options []string `xorm:"-" json:"options"`
|
||||
}
|
||||
|
||||
const (
|
||||
PeriodMonthly = "Monthly"
|
||||
PeriodYearly = "Yearly"
|
||||
)
|
||||
|
||||
func (plan *Plan) GetId() string {
|
||||
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) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Plan{})
|
||||
|
@ -32,12 +32,6 @@ type Pricing struct {
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
TrialDuration int `json:"trialDuration"`
|
||||
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 {
|
||||
|
@ -190,8 +190,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
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)
|
||||
plan, err := GetPlan(util.GetId(owner, planName))
|
||||
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 {
|
||||
return "", "", err
|
||||
}
|
||||
@ -267,14 +274,14 @@ 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),
|
||||
DisplayName: fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period),
|
||||
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),
|
||||
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,
|
||||
Tag: "auto_created_product_for_plan",
|
||||
Price: plan.PricePerMonth, // TODO
|
||||
Price: plan.Price,
|
||||
Currency: plan.Currency,
|
||||
|
||||
Quantity: 999,
|
||||
@ -290,9 +297,10 @@ func CreateProductForPlan(plan *Plan) *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.Owner = plan.Owner
|
||||
product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
|
||||
product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
|
||||
product.Price = plan.Price
|
||||
product.Currency = plan.Currency
|
||||
product.Providers = plan.PaymentProviders
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ type Subscription struct {
|
||||
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Duration int `json:"duration"`
|
||||
Period string `xorm:"varchar(100)" json:"period"`
|
||||
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
|
||||
@ -103,7 +103,8 @@ func (sub *Subscription) UpdateState() error {
|
||||
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]
|
||||
return &Subscription{
|
||||
Owner: owner,
|
||||
@ -112,13 +113,12 @@ func NewSubscription(owner, userName, pricingName, planName, paymentName string)
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
|
||||
User: userName,
|
||||
Pricing: pricingName,
|
||||
Plan: planName,
|
||||
Payment: paymentName,
|
||||
|
||||
StartTime: time.Now(),
|
||||
EndTime: time.Now().AddDate(0, 0, 30),
|
||||
Duration: 30, // TODO
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
Period: period,
|
||||
State: SubStatePending, // waiting for payment complete
|
||||
}
|
||||
}
|
||||
|
@ -197,22 +197,29 @@ class PlanEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<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 span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
|
||||
this.updatePlanField("pricePerMonth", value);
|
||||
<InputNumber value={this.state.plan.price} onChange={value => {
|
||||
this.updatePlanField("price", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<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 span={22} >
|
||||
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
|
||||
this.updatePlanField("pricePerYear", value);
|
||||
}} />
|
||||
<Select
|
||||
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>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
@ -32,9 +32,9 @@ class PlanListPage extends BaseListPage {
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Plan - ${randomName}`,
|
||||
description: "",
|
||||
pricePerMonth: 10,
|
||||
pricePerYear: 100,
|
||||
price: 10,
|
||||
currency: "USD",
|
||||
period: "Monthly",
|
||||
isEnabled: true,
|
||||
paymentProviders: [],
|
||||
role: "",
|
||||
@ -136,18 +136,18 @@ class PlanListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("currency"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per month"),
|
||||
dataIndex: "pricePerMonth",
|
||||
key: "pricePerMonth",
|
||||
title: i18next.t("plan:Price"),
|
||||
dataIndex: "price",
|
||||
key: "price",
|
||||
width: "130px",
|
||||
...this.getColumnSearchProps("pricePerMonth"),
|
||||
...this.getColumnSearchProps("price"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price per year"),
|
||||
dataIndex: "pricePerYear",
|
||||
key: "pricePerYear",
|
||||
title: i18next.t("plan:Period"),
|
||||
dataIndex: "period",
|
||||
key: "period",
|
||||
width: "130px",
|
||||
...this.getColumnSearchProps("pricePerYear"),
|
||||
...this.getColumnSearchProps("period"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Role"),
|
||||
|
@ -76,11 +76,10 @@ class ProductBuyPage extends React.Component {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
const plan = res.data;
|
||||
const productName = plan.product;
|
||||
await this.setStateAsync({
|
||||
pricing: pricing,
|
||||
plan: plan,
|
||||
productName: productName,
|
||||
productName: plan.product,
|
||||
});
|
||||
this.onUpdatePricing(pricing);
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
|
||||
}
|
||||
|
||||
renderProduct() {
|
||||
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
@ -112,7 +113,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<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>)
|
||||
}
|
||||
@ -124,7 +125,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@ -171,7 +172,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
})}>
|
||||
{
|
||||
@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
||||
</Col>
|
||||
<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);
|
||||
}} />
|
||||
</Col>
|
||||
@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
|
||||
{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.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>)
|
||||
}
|
||||
|
@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
|
||||
width: "230px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
|
||||
return (
|
||||
<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"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
disabled={isCreatedByPlan}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteProduct(index)}
|
||||
>
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import moment from "moment";
|
||||
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 PricingBackend from "./backend/PricingBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
@ -171,16 +171,6 @@ class SubscriptionEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
||||
@ -201,6 +191,23 @@ class SubscriptionEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
||||
|
@ -27,7 +27,6 @@ class SubscriptionListPage extends BaseListPage {
|
||||
newSubscription() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
const defaultDuration = 30;
|
||||
|
||||
return {
|
||||
owner: owner,
|
||||
@ -35,8 +34,8 @@ class SubscriptionListPage extends BaseListPage {
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Subscription - ${randomName}`,
|
||||
startTime: moment().format(),
|
||||
endTime: moment().add(defaultDuration, "d").format(),
|
||||
duration: defaultDuration,
|
||||
endTime: moment().add(30, "d").format(),
|
||||
period: "Monthly",
|
||||
description: "",
|
||||
user: "",
|
||||
plan: "",
|
||||
@ -130,11 +129,11 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Duration"),
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
title: i18next.t("subscription:Period"),
|
||||
dataIndex: "period",
|
||||
key: "period",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("duration"),
|
||||
...this.getColumnSearchProps("period"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Start time"),
|
||||
@ -150,20 +149,6 @@ class SubscriptionListPage extends BaseListPage {
|
||||
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"),
|
||||
dataIndex: "plan",
|
||||
|
@ -179,7 +179,6 @@ class SignupPage extends React.Component {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
values.plan = params.get("plan");
|
||||
values.pricing = params.get("pricing");
|
||||
|
||||
AuthBackend.signup(values)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 PlanBackend from "../backend/PlanBackend";
|
||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||
@ -33,6 +33,8 @@ class PricingPage extends React.Component {
|
||||
userName: params.get("user"),
|
||||
pricing: props.pricing,
|
||||
plans: null,
|
||||
periods: null,
|
||||
selectedPeriod: null,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
@ -73,8 +75,12 @@ class PricingPage extends React.Component {
|
||||
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
||||
return;
|
||||
}
|
||||
const plans = results.map(result => result.data);
|
||||
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
|
||||
this.setState({
|
||||
plans: results.map(result => result.data),
|
||||
plans: plans,
|
||||
periods: periods,
|
||||
selectedPeriod: periods?.[0],
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
@ -84,10 +90,9 @@ class PricingPage extends React.Component {
|
||||
}
|
||||
|
||||
loadPricing(pricingName) {
|
||||
if (pricingName === undefined) {
|
||||
if (!pricingName) {
|
||||
return;
|
||||
}
|
||||
|
||||
PricingBackend.getPricing(this.state.owner, pricingName)
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
@ -106,8 +111,31 @@ class PricingPage extends React.Component {
|
||||
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 pricing = this.state.pricing;
|
||||
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}}>
|
||||
{
|
||||
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} />
|
||||
);
|
||||
) : null;
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
@ -135,9 +163,9 @@ class PricingPage extends React.Component {
|
||||
<Row style={{justifyContent: "center"}} gutter={24}>
|
||||
{
|
||||
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} />
|
||||
);
|
||||
) : null;
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
@ -161,6 +189,13 @@ class PricingPage extends React.Component {
|
||||
<div className="login-form">
|
||||
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
||||
<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"}}>
|
||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||
{
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import {Button, Card, Col} from "antd";
|
||||
import {Button, Card, Col, Row} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
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"}}
|
||||
title={<h2>{plan.displayName}</h2>}
|
||||
>
|
||||
<Col>
|
||||
<Row>
|
||||
<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: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
||||
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.price}</span>
|
||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {plan.period === "Yearly" ? i18next.t("plan:per year") : i18next.t("plan:per month")}</span>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<br />
|
||||
<Row style={{height: "90px", paddingTop: "15px"}}>
|
||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||
<Meta description={plan.description} />
|
||||
</div>
|
||||
<br />
|
||||
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||
</Row>
|
||||
|
||||
{/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||
{(plan.options ?? []).map((option) => {
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
return <li>
|
||||
@ -58,15 +62,16 @@ class SingleCard extends React.Component {
|
||||
<span style={{fontSize: "16px"}}>{option}</span>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div style={{minHeight: "60px"}}>
|
||||
</ul> */}
|
||||
|
||||
</div>
|
||||
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
||||
<Row style={{paddingTop: "15px"}}>
|
||||
<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")
|
||||
}
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user