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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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