feat: add subscription managment (#1858)

* feat: subscription managment

* fix: remove console log

* fix: webhooks

* fix linter

* fix: fix via gofumpt

* fix: review changes

* fix: Copyright 2023

* Update account.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
Ilya Sulimanov
2023-05-20 10:56:21 +03:00
committed by GitHub
parent 319031da28
commit 88c0856d17
39 changed files with 4773 additions and 13 deletions

View File

@ -0,0 +1,167 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Card, Col, Row} from "antd";
import * as PricingBackend from "../backend/PricingBackend";
import * as PlanBackend from "../backend/PlanBackend";
import CustomGithubCorner from "../common/CustomGithubCorner";
import * as Setting from "../Setting";
import SingleCard from "./SingleCard";
import i18next from "i18next";
class PricingPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
applications: null,
pricingName: (props.pricingName ?? props.match?.params?.pricingName) ?? null,
pricing: props.pricing,
plans: null,
loading: false,
};
}
componentDidMount() {
this.setState({
applications: [],
});
if (this.state.pricing) {
this.loadPlans();
} else {
this.loadPricing(this.state.pricingName);
}
this.setState({
loading: true,
});
}
componentDidUpdate() {
if (this.state.pricing &&
this.state.pricing.plans?.length !== this.state.plans?.length && !this.state.loading) {
this.setState({loading: true});
this.loadPlans();
}
}
loadPlans() {
const plans = this.state.pricing.plans.map((plan) =>
PlanBackend.getPlanById(plan, true));
Promise.all(plans)
.then(results => {
this.setState({
plans: results,
loading: false,
});
})
.catch(error => {
Setting.showMessage("error", `Failed to get plans: ${error}`);
});
}
loadPricing(pricingName) {
if (pricingName === undefined) {
return;
}
PricingBackend.getPricing("built-in", pricingName)
.then((result) => {
this.setState({
loading: false,
pricing: result,
});
this.onUpdatePricing(result);
});
}
onUpdatePricing(pricing) {
this.props.onUpdatePricing(pricing);
}
renderCards() {
const getUrlByPlan = (plan) => {
const pricing = this.state.pricing;
const signUpUrl = `/signup/${pricing.application}?plan=${plan}&pricing=${pricing.name}`;
return `${window.location.origin}${signUpUrl}`;
};
if (Setting.isMobile()) {
return (
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
{
this.state.plans.map(item => {
return (
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
);
})
}
</Card>
);
} else {
return (
<div style={{marginRight: "15px", marginLeft: "15px"}}>
<Row style={{justifyContent: "center"}} gutter={24}>
{
this.state.plans.map(item => {
return (
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
);
})
}
</Row>
</div>
);
}
}
render() {
if (this.state.loading || this.state.plans === null || this.state.plans === undefined) {
return null;
}
const pricing = this.state.pricing;
return (
<React.Fragment>
<CustomGithubCorner />
<div className="login-content">
<div className="login-panel">
<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.renderCards()
}
</Col>
</Row>
<Row style={{justifyContent: "center"}}>
{pricing && pricing.trialDuration > 0
? <i>{i18next.t("pricing:Free")} {pricing.trialDuration}-{i18next.t("pricing:days trial available!")}</i>
: null}
</Row>
</div>
</div>
</div>
</React.Fragment>
);
}
}
export default PricingPage;

View File

@ -0,0 +1,85 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import i18next from "i18next";
import React from "react";
import {Button, Card, Col} from "antd";
import * as Setting from "../Setting";
import {withRouter} from "react-router-dom";
const {Meta} = Card;
class SingleCard extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
renderCard(plan, isSingle, link) {
return (
<Col style={{minWidth: "320px", paddingLeft: "20px", paddingRight: "20px", paddingBottom: "20px", marginBottom: "20px", paddingTop: "0px"}} span={6}>
<Card
hoverable
onClick={() => Setting.isMobile() ? window.location.href = link : null}
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
>
<div style={{textAlign: "right"}}>
<h2
style={{marginTop: "0px"}}>{plan.displayName}</h2>
</div>
<div style={{textAlign: "left"}} className="px-10 mt-5">
<span style={{fontWeight: 700, fontSize: "48px"}}>$ {plan.pricePerMonth}</span>
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:PerMonth")}</span>
</div>
<br />
<div style={{textAlign: "left", fontSize: "18px"}}>
<Meta description={plan.description} />
</div>
<br />
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
{(plan.options ?? []).map((option) => {
// eslint-disable-next-line react/jsx-key
return <li>
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
</svg>
<span style={{fontSize: "16px"}}>{option}</span>
</li>;
})}
</ul>
<div style={{minHeight: "60px"}}>
</div>
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
{
i18next.t("pricing:Getting started")
}
</Button>
</Card>
</Col>
);
}
render() {
return this.renderCard(this.props.plan, this.props.isSingle, this.props.link);
}
}
export default withRouter(SingleCard);