2022-02-27 23:50:35 +08:00
|
|
|
// Copyright 2022 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";
|
2024-06-22 14:05:53 +08:00
|
|
|
import {Button, Descriptions, InputNumber, Space, Spin} from "antd";
|
2022-02-27 23:50:35 +08:00
|
|
|
import i18next from "i18next";
|
|
|
|
import * as ProductBackend from "./backend/ProductBackend";
|
2023-08-24 23:20:50 +08:00
|
|
|
import * as PlanBackend from "./backend/PlanBackend";
|
|
|
|
import * as PricingBackend from "./backend/PricingBackend";
|
2022-03-06 22:46:02 +08:00
|
|
|
import * as Setting from "./Setting";
|
2022-02-27 23:50:35 +08:00
|
|
|
|
|
|
|
class ProductBuyPage extends React.Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2023-08-24 23:20:50 +08:00
|
|
|
const params = new URLSearchParams(window.location.search);
|
2022-02-27 23:50:35 +08:00
|
|
|
this.state = {
|
|
|
|
classes: props,
|
2023-08-24 23:20:50 +08:00
|
|
|
owner: props?.organizationName ?? props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
|
|
|
|
productName: props?.productName ?? props?.match?.params?.productName ?? null,
|
|
|
|
pricingName: props?.pricingName ?? props?.match?.params?.pricingName ?? null,
|
|
|
|
planName: params.get("plan"),
|
|
|
|
userName: params.get("user"),
|
2023-11-11 17:16:57 +08:00
|
|
|
paymentEnv: "",
|
2022-02-27 23:50:35 +08:00
|
|
|
product: null,
|
2023-08-24 23:20:50 +08:00
|
|
|
pricing: props?.pricing ?? null,
|
|
|
|
plan: null,
|
2022-03-07 00:33:45 +08:00
|
|
|
isPlacingOrder: false,
|
2024-06-22 14:05:53 +08:00
|
|
|
customPrice: 0,
|
2022-02-27 23:50:35 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-11-11 17:16:57 +08:00
|
|
|
getPaymentEnv() {
|
|
|
|
let env = "";
|
|
|
|
const ua = navigator.userAgent.toLocaleLowerCase();
|
|
|
|
// Only support Wechat Pay in Wechat Browser for mobile devices
|
|
|
|
if (ua.indexOf("micromessenger") !== -1 && ua.indexOf("mobile") !== -1) {
|
|
|
|
env = "WechatBrowser";
|
|
|
|
}
|
|
|
|
this.setState({
|
|
|
|
paymentEnv: env,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-27 23:50:35 +08:00
|
|
|
UNSAFE_componentWillMount() {
|
|
|
|
this.getProduct();
|
2023-11-11 17:16:57 +08:00
|
|
|
this.getPaymentEnv();
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:20:50 +08:00
|
|
|
setStateAsync(state) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.setState(state, () => {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onUpdatePricing(pricing) {
|
|
|
|
this.props.onUpdatePricing(pricing);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProduct() {
|
|
|
|
if (!this.state.owner || (!this.state.productName && !this.state.pricingName)) {
|
2023-07-30 11:54:42 +08:00
|
|
|
return ;
|
2022-12-16 23:09:48 +08:00
|
|
|
}
|
2023-08-24 23:20:50 +08:00
|
|
|
try {
|
|
|
|
// load pricing & plan
|
|
|
|
if (this.state.pricingName) {
|
|
|
|
if (!this.state.planName || !this.state.userName) {
|
|
|
|
return ;
|
2023-06-27 20:33:47 +07:00
|
|
|
}
|
2023-08-24 23:20:50 +08:00
|
|
|
let res = await PricingBackend.getPricing(this.state.owner, this.state.pricingName);
|
|
|
|
if (res.status !== "ok") {
|
|
|
|
throw new Error(res.msg);
|
|
|
|
}
|
|
|
|
const pricing = res.data;
|
|
|
|
res = await PlanBackend.getPlan(this.state.owner, this.state.planName);
|
|
|
|
if (res.status !== "ok") {
|
|
|
|
throw new Error(res.msg);
|
|
|
|
}
|
|
|
|
const plan = res.data;
|
|
|
|
await this.setStateAsync({
|
|
|
|
pricing: pricing,
|
|
|
|
plan: plan,
|
2023-08-30 17:13:45 +08:00
|
|
|
productName: plan.product,
|
2022-02-27 23:50:35 +08:00
|
|
|
});
|
2023-08-24 23:20:50 +08:00
|
|
|
this.onUpdatePricing(pricing);
|
|
|
|
}
|
|
|
|
// load product
|
|
|
|
const res = await ProductBackend.getProduct(this.state.owner, this.state.productName);
|
|
|
|
if (res.status !== "ok") {
|
|
|
|
throw new Error(res.msg);
|
|
|
|
}
|
|
|
|
this.setState({
|
|
|
|
product: res.data,
|
2022-02-27 23:50:35 +08:00
|
|
|
});
|
2023-08-24 23:20:50 +08:00
|
|
|
} catch (err) {
|
|
|
|
Setting.showMessage("error", err.message);
|
|
|
|
return;
|
|
|
|
}
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
getProductObj() {
|
|
|
|
if (this.props.product !== undefined) {
|
|
|
|
return this.props.product;
|
|
|
|
} else {
|
|
|
|
return this.state.product;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getCurrencySymbol(product) {
|
|
|
|
if (product?.currency === "USD") {
|
|
|
|
return "$";
|
|
|
|
} else if (product?.currency === "CNY") {
|
|
|
|
return "¥";
|
2024-12-01 21:46:44 +08:00
|
|
|
} else if (product?.currency === "EUR") {
|
|
|
|
return "€";
|
|
|
|
} else if (product?.currency === "JPY") {
|
|
|
|
return "¥";
|
|
|
|
} else if (product?.currency === "GBP") {
|
|
|
|
return "£";
|
|
|
|
} else if (product?.currency === "AUD") {
|
|
|
|
return "A$";
|
|
|
|
} else if (product?.currency === "CAD") {
|
|
|
|
return "C$";
|
|
|
|
} else if (product?.currency === "CHF") {
|
|
|
|
return "CHF";
|
|
|
|
} else if (product?.currency === "HKD") {
|
|
|
|
return "HK$";
|
|
|
|
} else if (product?.currency === "SGD") {
|
|
|
|
return "S$";
|
2025-04-11 22:24:34 +08:00
|
|
|
} else if (product?.currency === "BRL") {
|
|
|
|
return "R$";
|
2022-02-27 23:50:35 +08:00
|
|
|
} else {
|
|
|
|
return "(Unknown currency)";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-26 16:42:25 +08:00
|
|
|
getPrice(product) {
|
2024-06-22 14:05:53 +08:00
|
|
|
return `${this.getCurrencySymbol(product)}${product?.price} (${Setting.getCurrencyText(product)})`;
|
2022-03-26 16:42:25 +08:00
|
|
|
}
|
|
|
|
|
2023-11-11 17:16:57 +08:00
|
|
|
// Call Weechat Pay via jsapi
|
|
|
|
onBridgeReady(attachInfo) {
|
|
|
|
const {WeixinJSBridge} = window;
|
|
|
|
// Setting.showMessage("success", "attachInfo is " + JSON.stringify(attachInfo));
|
|
|
|
this.setState({
|
|
|
|
isPlacingOrder: false,
|
|
|
|
});
|
|
|
|
WeixinJSBridge.invoke(
|
|
|
|
"getBrandWCPayRequest", {
|
|
|
|
"appId": attachInfo.appId,
|
|
|
|
"timeStamp": attachInfo.timeStamp,
|
|
|
|
"nonceStr": attachInfo.nonceStr,
|
|
|
|
"package": attachInfo.package,
|
|
|
|
"signType": attachInfo.signType,
|
|
|
|
"paySign": attachInfo.paySign,
|
|
|
|
},
|
|
|
|
function(res) {
|
|
|
|
if (res.err_msg === "get_brand_wcpay_request:ok") {
|
|
|
|
Setting.goToLink(attachInfo.payment.successUrl);
|
|
|
|
return ;
|
|
|
|
} else {
|
|
|
|
if (res.err_msg === "get_brand_wcpay_request:cancel") {
|
|
|
|
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
|
|
|
|
} else {
|
|
|
|
Setting.showMessage("error", i18next.t("product:Payment failed"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// In Wechat browser, call this function to pay via jsapi
|
|
|
|
callWechatPay(attachInfo) {
|
|
|
|
const {WeixinJSBridge} = window;
|
|
|
|
if (typeof WeixinJSBridge === "undefined") {
|
|
|
|
if (document.addEventListener) {
|
|
|
|
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
|
|
|
|
} else if (document.attachEvent) {
|
|
|
|
document.attachEvent("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
|
|
|
|
document.attachEvent("onWeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.onBridgeReady(attachInfo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-06 22:46:02 +08:00
|
|
|
buyProduct(product, provider) {
|
2022-03-07 00:33:45 +08:00
|
|
|
this.setState({
|
|
|
|
isPlacingOrder: true,
|
|
|
|
});
|
|
|
|
|
2024-06-22 14:05:53 +08:00
|
|
|
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv, this.state.customPrice)
|
2022-03-06 22:46:02 +08:00
|
|
|
.then((res) => {
|
2022-12-02 00:06:28 +08:00
|
|
|
if (res.status === "ok") {
|
2023-09-07 15:45:54 +08:00
|
|
|
const payment = res.data;
|
2023-11-11 17:16:57 +08:00
|
|
|
const attachInfo = res.data2;
|
2023-09-07 15:45:54 +08:00
|
|
|
let payUrl = payment.payUrl;
|
|
|
|
if (provider.type === "WeChat Pay") {
|
2023-11-11 17:16:57 +08:00
|
|
|
if (this.state.paymentEnv === "WechatBrowser") {
|
|
|
|
attachInfo.payment = payment;
|
|
|
|
this.callWechatPay(attachInfo);
|
|
|
|
return ;
|
|
|
|
}
|
2023-09-07 15:45:54 +08:00
|
|
|
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURI(payment.payUrl)}&successUrl=${encodeURI(payment.successUrl)}`;
|
|
|
|
}
|
2022-03-06 22:46:02 +08:00
|
|
|
Setting.goToLink(payUrl);
|
|
|
|
} else {
|
2022-12-02 00:06:28 +08:00
|
|
|
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
2022-03-07 00:33:45 +08:00
|
|
|
this.setState({
|
|
|
|
isPlacingOrder: false,
|
|
|
|
});
|
2022-03-06 22:46:02 +08:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
2022-12-02 00:06:28 +08:00
|
|
|
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
2022-03-06 22:46:02 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-27 23:50:35 +08:00
|
|
|
getPayButton(provider) {
|
|
|
|
let text = provider.type;
|
2023-05-30 23:25:58 +08:00
|
|
|
if (provider.type === "Dummy") {
|
|
|
|
text = i18next.t("product:Dummy");
|
|
|
|
} else if (provider.type === "Alipay") {
|
2022-02-27 23:50:35 +08:00
|
|
|
text = i18next.t("product:Alipay");
|
|
|
|
} else if (provider.type === "WeChat Pay") {
|
|
|
|
text = i18next.t("product:WeChat Pay");
|
2023-03-18 18:54:05 +08:00
|
|
|
} else if (provider.type === "PayPal") {
|
|
|
|
text = i18next.t("product:PayPal");
|
2023-08-15 00:16:30 +08:00
|
|
|
} else if (provider.type === "Stripe") {
|
|
|
|
text = i18next.t("product:Stripe");
|
2025-02-07 19:19:30 +08:00
|
|
|
} else if (provider.type === "AirWallex") {
|
|
|
|
text = i18next.t("product:AirWallex");
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
|
2022-04-21 21:52:34 +08:00
|
|
|
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
2022-02-27 23:50:35 +08:00
|
|
|
} size={"large"} >
|
|
|
|
{
|
|
|
|
text
|
|
|
|
}
|
|
|
|
</Button>
|
2022-07-10 15:45:55 +08:00
|
|
|
);
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
renderProviderButton(provider, product) {
|
|
|
|
return (
|
|
|
|
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
|
2022-03-06 22:46:02 +08:00
|
|
|
<span style={{width: "200px", cursor: "pointer"}} onClick={() => this.buyProduct(product, provider)}>
|
2022-02-27 23:50:35 +08:00
|
|
|
{
|
|
|
|
this.getPayButton(provider)
|
|
|
|
}
|
2022-03-06 22:46:02 +08:00
|
|
|
</span>
|
2022-02-27 23:50:35 +08:00
|
|
|
</span>
|
2022-07-10 15:45:55 +08:00
|
|
|
);
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
renderPay(product) {
|
|
|
|
if (product === undefined || product === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (product.state !== "Published") {
|
|
|
|
return i18next.t("product:This product is currently not in sale.");
|
|
|
|
}
|
2022-08-07 15:44:57 +08:00
|
|
|
if (product.providerObjs.length === 0) {
|
2022-02-27 23:50:35 +08:00
|
|
|
return i18next.t("product:There is no payment channel for this product.");
|
|
|
|
}
|
|
|
|
|
2022-08-07 15:44:57 +08:00
|
|
|
return product.providerObjs.map(provider => {
|
2022-02-27 23:50:35 +08:00
|
|
|
return this.renderProviderButton(provider, product);
|
2022-07-10 15:45:55 +08:00
|
|
|
});
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const product = this.getProductObj();
|
|
|
|
|
2022-03-13 16:25:54 +08:00
|
|
|
if (product === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-02-27 23:50:35 +08:00
|
|
|
return (
|
2023-08-24 23:20:50 +08:00
|
|
|
<div className="login-content">
|
2022-03-07 00:33:45 +08:00
|
|
|
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
|
2023-11-11 17:16:57 +08:00
|
|
|
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 20} : {fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
|
2022-03-07 00:33:45 +08:00
|
|
|
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
2023-08-24 23:20:50 +08:00
|
|
|
<span style={{fontSize: 25}}>
|
2022-12-07 01:53:03 +08:00
|
|
|
{Setting.getLanguageText(product?.displayName)}
|
2022-07-10 15:45:55 +08:00
|
|
|
</span>
|
2022-03-07 00:33:45 +08:00
|
|
|
</Descriptions.Item>
|
2022-12-07 01:53:03 +08:00
|
|
|
<Descriptions.Item label={i18next.t("product:Detail")}><span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span></Descriptions.Item>
|
2023-03-19 01:01:39 +08:00
|
|
|
<Descriptions.Item label={i18next.t("user:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
|
2022-03-07 00:33:45 +08:00
|
|
|
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
|
|
|
|
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
2022-07-10 15:45:55 +08:00
|
|
|
<img src={product?.image} alt={product?.name} height={90} style={{marginBottom: "20px"}} />
|
2022-03-07 00:33:45 +08:00
|
|
|
</Descriptions.Item>
|
2024-06-22 14:05:53 +08:00
|
|
|
{
|
|
|
|
product.isRecharge ? (
|
|
|
|
<Descriptions.Item span={3} label={i18next.t("product:Price")}>
|
|
|
|
<Space>
|
|
|
|
<InputNumber min={0} value={this.state.customPrice} onChange={(e) => {this.setState({customPrice: e});}} /> {Setting.getCurrencyText(product)}
|
|
|
|
</Space>
|
|
|
|
</Descriptions.Item>
|
|
|
|
) : (
|
|
|
|
<React.Fragment>
|
|
|
|
<Descriptions.Item label={i18next.t("product:Price")}>
|
|
|
|
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
|
|
|
|
{
|
|
|
|
this.getPrice(product)
|
|
|
|
}
|
|
|
|
</span>
|
|
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item>
|
|
|
|
<Descriptions.Item label={i18next.t("product:Sold")}><span style={{fontSize: 16}}>{product?.sold}</span></Descriptions.Item>
|
|
|
|
</React.Fragment>
|
|
|
|
)
|
|
|
|
}
|
2022-03-07 00:33:45 +08:00
|
|
|
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
|
|
|
|
{
|
|
|
|
this.renderPay(product)
|
|
|
|
}
|
|
|
|
</Descriptions.Item>
|
|
|
|
</Descriptions>
|
|
|
|
</Spin>
|
2022-02-27 23:50:35 +08:00
|
|
|
</div>
|
2022-07-10 15:45:55 +08:00
|
|
|
);
|
2022-02-27 23:50:35 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ProductBuyPage;
|