mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-05 14:09:57 +08:00
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:
167
web/src/pricing/PricingPage.js
Normal file
167
web/src/pricing/PricingPage.js
Normal 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;
|
85
web/src/pricing/SingleCard.js
Normal file
85
web/src/pricing/SingleCard.js
Normal 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);
|
Reference in New Issue
Block a user