mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 10:45:47 +08:00
feat: support wechat pay (#2312)
* feat: support wechat pay * feat: support wechat pay * feat: update wechatpay.go * feat: add router /qrcode
This commit is contained in:
parent
7318ee6e3a
commit
16cd09d175
@ -88,6 +88,7 @@ p, *, *, *, /api/metrics, *, *
|
|||||||
p, *, *, GET, /api/get-pricing, *, *
|
p, *, *, GET, /api/get-pricing, *, *
|
||||||
p, *, *, GET, /api/get-plan, *, *
|
p, *, *, GET, /api/get-plan, *, *
|
||||||
p, *, *, GET, /api/get-subscription, *, *
|
p, *, *, GET, /api/get-subscription, *, *
|
||||||
|
p, *, *, GET, /api/get-provider, *, *
|
||||||
p, *, *, GET, /api/get-organization-names, *, *
|
p, *, *, GET, /api/get-organization-names, *, *
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -176,11 +176,10 @@ func (c *ApiController) DeletePayment() {
|
|||||||
func (c *ApiController) NotifyPayment() {
|
func (c *ApiController) NotifyPayment() {
|
||||||
owner := c.Ctx.Input.Param(":owner")
|
owner := c.Ctx.Input.Param(":owner")
|
||||||
paymentName := c.Ctx.Input.Param(":payment")
|
paymentName := c.Ctx.Input.Param(":payment")
|
||||||
orderId := c.Ctx.Input.Param("order")
|
|
||||||
|
|
||||||
body := c.Ctx.Input.RequestBody
|
body := c.Ctx.Input.RequestBody
|
||||||
|
|
||||||
payment, err := object.NotifyPayment(c.Ctx.Request, body, owner, paymentName, orderId)
|
payment, err := object.NotifyPayment(c.Ctx.Request, body, owner, paymentName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -187,11 +187,11 @@ func (c *ApiController) BuyProduct() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
|
payment, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ResponseOk(payUrl, orderId)
|
c.ResponseOk(payment)
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ type Payment struct {
|
|||||||
// Order Info
|
// Order Info
|
||||||
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
|
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
|
||||||
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
|
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
|
||||||
|
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl""` // `successUrl` is redirected from `payUrl` after pay success
|
||||||
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
||||||
Message string `xorm:"varchar(2000)" json:"message"`
|
Message string `xorm:"varchar(2000)" json:"message"`
|
||||||
}
|
}
|
||||||
@ -152,7 +153,7 @@ func DeletePayment(payment *Payment) (bool, error) {
|
|||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, *pp.NotifyResult, error) {
|
func notifyPayment(request *http.Request, body []byte, owner string, paymentName string) (*Payment, *pp.NotifyResult, error) {
|
||||||
payment, err := getPayment(owner, paymentName)
|
payment, err := getPayment(owner, paymentName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -180,11 +181,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if orderId == "" {
|
notifyResult, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, payment.OutOrderId)
|
||||||
orderId = payment.OutOrderId
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyResult, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return payment, nil, err
|
return payment, nil, err
|
||||||
}
|
}
|
||||||
@ -205,8 +202,8 @@ func notifyPayment(request *http.Request, body []byte, owner string, paymentName
|
|||||||
return payment, notifyResult, nil
|
return payment, notifyResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, error) {
|
func NotifyPayment(request *http.Request, body []byte, owner string, paymentName string) (*Payment, error) {
|
||||||
payment, notifyResult, err := notifyPayment(request, body, owner, paymentName, orderId)
|
payment, notifyResult, err := notifyPayment(request, body, owner, paymentName)
|
||||||
if payment != nil {
|
if payment != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
payment.State = pp.PaymentStateError
|
payment.State = pp.PaymentStateError
|
||||||
|
@ -158,24 +158,23 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
|
|||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (string, string, error) {
|
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
|
||||||
product, err := GetProduct(id)
|
product, err := GetProduct(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if product == nil {
|
if product == nil {
|
||||||
return "", "", fmt.Errorf("the product: %s does not exist", id)
|
return nil, fmt.Errorf("the product: %s does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := product.getProvider(providerName)
|
provider, err := product.getProvider(providerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pProvider, _, err := provider.getPaymentProvider()
|
pProvider, _, err := provider.getPaymentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
owner := product.Owner
|
owner := product.Owner
|
||||||
@ -192,15 +191,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
if pricingName != "" && planName != "" {
|
if pricingName != "" && planName != "" {
|
||||||
plan, err := GetPlan(util.GetId(owner, planName))
|
plan, err := GetPlan(util.GetId(owner, planName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
if plan == nil {
|
if plan == nil {
|
||||||
return "", "", fmt.Errorf("the plan: %s does not exist", planName)
|
return nil, fmt.Errorf("the plan: %s does not exist", planName)
|
||||||
}
|
}
|
||||||
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||||
_, err = AddSubscription(sub)
|
_, err = AddSubscription(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
|
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
|
||||||
}
|
}
|
||||||
@ -208,10 +207,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
// Create an OrderId and get the payUrl
|
// Create an OrderId and get the payUrl
|
||||||
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
|
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Create a Payment linked with Product and Order
|
// Create a Payment linked with Product and Order
|
||||||
payment := Payment{
|
payment := &Payment{
|
||||||
Owner: product.Owner,
|
Owner: product.Owner,
|
||||||
Name: paymentName,
|
Name: paymentName,
|
||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
@ -230,6 +229,7 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
|
|
||||||
User: user.Name,
|
User: user.Name,
|
||||||
PayUrl: payUrl,
|
PayUrl: payUrl,
|
||||||
|
SuccessUrl: returnUrl,
|
||||||
State: pp.PaymentStateCreated,
|
State: pp.PaymentStateCreated,
|
||||||
OutOrderId: orderId,
|
OutOrderId: orderId,
|
||||||
}
|
}
|
||||||
@ -238,15 +238,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
payment.State = pp.PaymentStatePaid
|
payment.State = pp.PaymentStatePaid
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := AddPayment(&payment)
|
affected, err := AddPayment(payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !affected {
|
if !affected {
|
||||||
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||||
}
|
}
|
||||||
return payUrl, orderId, err
|
return payment, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtendProductWithProviders(product *Product) error {
|
func ExtendProductWithProviders(product *Product) error {
|
||||||
|
@ -17,6 +17,7 @@ package pp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -31,17 +32,22 @@ type WechatPayNotifyResponse struct {
|
|||||||
|
|
||||||
type WechatPaymentProvider struct {
|
type WechatPaymentProvider struct {
|
||||||
Client *wechat.ClientV3
|
Client *wechat.ClientV3
|
||||||
appId string
|
AppId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCertSerialNumber string, privateKey string) (*WechatPaymentProvider, error) {
|
func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, serialNo string, privateKey string) (*WechatPaymentProvider, error) {
|
||||||
if appId == "" && mchId == "" && mchCertSerialNumber == "" && apiV3Key == "" && privateKey == "" {
|
// https://pay.weixin.qq.com/docs/merchant/products/native-payment/preparation.html
|
||||||
|
// clientId => mchId
|
||||||
|
// clientSecret => apiV3Key
|
||||||
|
// clientId2 => appId
|
||||||
|
|
||||||
|
// appCertificate => serialNo
|
||||||
|
// appPrivateKey => privateKey
|
||||||
|
if appId == "" || mchId == "" || serialNo == "" || apiV3Key == "" || privateKey == "" {
|
||||||
return &WechatPaymentProvider{}, nil
|
return &WechatPaymentProvider{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pp := &WechatPaymentProvider{appId: appId}
|
clientV3, err := wechat.NewClientV3(mchId, serialNo, apiV3Key, privateKey)
|
||||||
|
|
||||||
clientV3, err := wechat.NewClientV3(mchId, mchCertSerialNumber, apiV3Key, privateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -50,73 +56,70 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
pp := &WechatPaymentProvider{
|
||||||
pp.Client = clientV3.SetPlatformCert([]byte(platformCert), serialNo)
|
Client: clientV3.SetPlatformCert([]byte(platformCert), serialNo),
|
||||||
|
AppId: appId,
|
||||||
|
}
|
||||||
|
|
||||||
return pp, nil
|
return pp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
|
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
|
||||||
// pp.Client.DebugSwitch = gopay.DebugOn
|
|
||||||
|
|
||||||
bm := gopay.BodyMap{}
|
bm := gopay.BodyMap{}
|
||||||
|
|
||||||
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
|
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
|
||||||
bm.Set("appid", pp.appId)
|
bm.Set("appid", pp.AppId)
|
||||||
bm.Set("description", productDisplayName)
|
bm.Set("description", productDisplayName)
|
||||||
bm.Set("notify_url", notifyUrl)
|
bm.Set("notify_url", notifyUrl)
|
||||||
bm.Set("out_trade_no", paymentName)
|
bm.Set("out_trade_no", paymentName)
|
||||||
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
|
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||||
bm.Set("total", int(price*100))
|
bm.Set("total", priceFloat64ToInt64(price))
|
||||||
bm.Set("currency", "CNY")
|
bm.Set("currency", currency)
|
||||||
})
|
})
|
||||||
|
|
||||||
wxRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
|
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
if nativeRsp.Code != wechat.Success {
|
||||||
if wxRsp.Code != wechat.Success {
|
return "", "", errors.New(nativeRsp.Error)
|
||||||
return "", "", errors.New(wxRsp.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return wxRsp.Response.CodeUrl, "", nil
|
return nativeRsp.Response.CodeUrl, paymentName, nil // Wechat can use paymentName as the OutTradeNo to query order status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
|
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
|
||||||
notifyReq, err := wechat.V3ParseNotify(request)
|
notifyResult := &NotifyResult{}
|
||||||
|
queryRsp, err := pp.Client.V3TransactionQueryOrder(context.Background(), wechat.OutTradeNo, orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if queryRsp.Code != wechat.Success {
|
||||||
cert := pp.Client.WxPublicKey()
|
return nil, errors.New(queryRsp.Error)
|
||||||
err = notifyReq.VerifySignByPK(cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := string(pp.Client.ApiV3Key)
|
switch queryRsp.Response.TradeState {
|
||||||
result, err := notifyReq.DecryptCipherText(apiKey)
|
case "SUCCESS":
|
||||||
if err != nil {
|
// skip
|
||||||
return nil, err
|
case "CLOSED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||||
|
return notifyResult, nil
|
||||||
|
case "NOTPAY", "USERPAYING": // not-pad: waiting for user to pay; user-paying: user is paying
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCreated
|
||||||
|
return notifyResult, nil
|
||||||
|
default:
|
||||||
|
notifyResult.PaymentStatus = PaymentStateError
|
||||||
|
notifyResult.NotifyMessage = fmt.Sprintf("unexpected wechat trade state: %v", queryRsp.Response.TradeState)
|
||||||
|
return notifyResult, nil
|
||||||
}
|
}
|
||||||
|
productDisplayName, productName, providerName, _ := parseAttachString(queryRsp.Response.Attach)
|
||||||
paymentName := result.OutTradeNo
|
notifyResult = &NotifyResult{
|
||||||
price := float64(result.Amount.PayerTotal) / 100
|
|
||||||
|
|
||||||
productDisplayName, productName, providerName, err := parseAttachString(result.Attach)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyResult := &NotifyResult{
|
|
||||||
ProductName: productName,
|
ProductName: productName,
|
||||||
ProductDisplayName: productDisplayName,
|
ProductDisplayName: productDisplayName,
|
||||||
ProviderName: providerName,
|
ProviderName: providerName,
|
||||||
OrderId: orderId,
|
OrderId: orderId,
|
||||||
Price: price,
|
Price: priceInt64ToFloat64(int64(queryRsp.Response.Amount.Total)),
|
||||||
PaymentStatus: PaymentStatePaid,
|
PaymentStatus: PaymentStatePaid,
|
||||||
PaymentName: paymentName,
|
PaymentName: queryRsp.Response.OutTradeNo,
|
||||||
}
|
}
|
||||||
return notifyResult, nil
|
return notifyResult, nil
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"i18next": "^19.8.9",
|
"i18next": "^19.8.9",
|
||||||
"libphonenumber-js": "^1.10.19",
|
"libphonenumber-js": "^1.10.19",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
@ -82,6 +83,9 @@
|
|||||||
"@babel/eslint-parser": "^7.18.9",
|
"@babel/eslint-parser": "^7.18.9",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^12.5.1",
|
"cypress": "^12.5.1",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.22.0",
|
||||||
@ -91,10 +95,7 @@
|
|||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"stylelint": "^14.11.0",
|
"stylelint": "^14.11.0",
|
||||||
"stylelint-config-recommended-less": "^1.0.4",
|
"stylelint-config-recommended-less": "^1.0.4",
|
||||||
"stylelint-config-standard": "^28.0.0",
|
"stylelint-config-standard": "^28.0.0"
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
|
||||||
"@testing-library/react": "^9.3.2",
|
|
||||||
"@testing-library/user-event": "^7.1.2"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{css,less}": [
|
"src/**/*.{css,less}": [
|
||||||
|
@ -664,7 +664,8 @@ class App extends Component {
|
|||||||
window.location.pathname.startsWith("/cas") ||
|
window.location.pathname.startsWith("/cas") ||
|
||||||
window.location.pathname.startsWith("/auto-signup") ||
|
window.location.pathname.startsWith("/auto-signup") ||
|
||||||
window.location.pathname.startsWith("/select-plan") ||
|
window.location.pathname.startsWith("/select-plan") ||
|
||||||
window.location.pathname.startsWith("/buy-plan");
|
window.location.pathname.startsWith("/buy-plan") ||
|
||||||
|
window.location.pathname.startsWith("/qrcode") ;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
|
@ -31,6 +31,7 @@ import CasLogout from "./auth/CasLogout";
|
|||||||
import {authConfig} from "./auth/Auth";
|
import {authConfig} from "./auth/Auth";
|
||||||
import ProductBuyPage from "./ProductBuyPage";
|
import ProductBuyPage from "./ProductBuyPage";
|
||||||
import PaymentResultPage from "./PaymentResultPage";
|
import PaymentResultPage from "./PaymentResultPage";
|
||||||
|
import QrCodePage from "./QrCodePage";
|
||||||
|
|
||||||
class EntryPage extends React.Component {
|
class EntryPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -113,6 +114,7 @@ class EntryPage extends React.Component {
|
|||||||
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => <PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
<Route exact path="/select-plan/:owner/:pricingName" render={(props) => <PricingPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
|
||||||
|
<Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Descriptions, Modal, Spin} from "antd";
|
import {Button, Descriptions, Spin} from "antd";
|
||||||
import {CheckCircleTwoTone} from "@ant-design/icons";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import * as ProductBackend from "./backend/ProductBackend";
|
import * as ProductBackend from "./backend/ProductBackend";
|
||||||
import * as PlanBackend from "./backend/PlanBackend";
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
@ -36,7 +35,6 @@ class ProductBuyPage extends React.Component {
|
|||||||
pricing: props?.pricing ?? null,
|
pricing: props?.pricing ?? null,
|
||||||
plan: null,
|
plan: null,
|
||||||
isPlacingOrder: false,
|
isPlacingOrder: false,
|
||||||
qrCodeModalProvider: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,13 +128,6 @@ class ProductBuyPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buyProduct(product, provider) {
|
buyProduct(product, provider) {
|
||||||
if (provider.clientId.startsWith("http")) {
|
|
||||||
this.setState({
|
|
||||||
qrCodeModalProvider: provider,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isPlacingOrder: true,
|
isPlacingOrder: true,
|
||||||
});
|
});
|
||||||
@ -144,7 +135,11 @@ class ProductBuyPage extends React.Component {
|
|||||||
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
|
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const payUrl = res.data;
|
const payment = res.data;
|
||||||
|
let payUrl = payment.payUrl;
|
||||||
|
if (provider.type === "WeChat Pay") {
|
||||||
|
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURI(payment.payUrl)}&successUrl=${encodeURI(payment.successUrl)}`;
|
||||||
|
}
|
||||||
Setting.goToLink(payUrl);
|
Setting.goToLink(payUrl);
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||||
@ -159,45 +154,6 @@ class ProductBuyPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderQrCodeModal() {
|
|
||||||
if (this.state.qrCodeModalProvider === undefined || this.state.qrCodeModalProvider === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal title={
|
|
||||||
<div>
|
|
||||||
<CheckCircleTwoTone twoToneColor="rgb(45,120,213)" />
|
|
||||||
{" " + i18next.t("product:Please scan the QR code to pay")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
open={this.state.qrCodeModalProvider !== undefined && this.state.qrCodeModalProvider !== null}
|
|
||||||
onOk={() => {
|
|
||||||
Setting.goToLink(this.state.product.returnUrl);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
this.setState({
|
|
||||||
qrCodeModalProvider: null,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
okText={i18next.t("product:I have completed the payment")}
|
|
||||||
cancelText={i18next.t("general:Cancel")}>
|
|
||||||
<p key={this.state.qrCodeModalProvider?.name}>
|
|
||||||
{
|
|
||||||
i18next.t("product:Please provide your username in the remark")
|
|
||||||
}
|
|
||||||
:
|
|
||||||
{
|
|
||||||
Setting.getTag("default", this.props.account.name)
|
|
||||||
}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<img src={this.state.qrCodeModalProvider?.clientId} alt={this.state.qrCodeModalProvider?.name} width={"472px"} style={{marginBottom: "20px"}} />
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPayButton(provider) {
|
getPayButton(provider) {
|
||||||
let text = provider.type;
|
let text = provider.type;
|
||||||
if (provider.type === "Dummy") {
|
if (provider.type === "Dummy") {
|
||||||
@ -290,9 +246,6 @@ class ProductBuyPage extends React.Component {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Spin>
|
</Spin>
|
||||||
{
|
|
||||||
this.renderQrCodeModal()
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import React from "react";
|
|||||||
import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
||||||
import {LinkOutlined} from "@ant-design/icons";
|
import {LinkOutlined} from "@ant-design/icons";
|
||||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||||
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
|
import * as CertBackend from "./backend/CertBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {authConfig} from "./auth/Auth";
|
import {authConfig} from "./auth/Auth";
|
||||||
@ -24,7 +26,6 @@ import * as ProviderNotification from "./common/TestNotificationWidget";
|
|||||||
import * as ProviderEditTestSms from "./common/TestSmsWidget";
|
import * as ProviderEditTestSms from "./common/TestSmsWidget";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import {CaptchaPreview} from "./common/CaptchaPreview";
|
import {CaptchaPreview} from "./common/CaptchaPreview";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
|
||||||
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
||||||
import * as Web3Auth from "./auth/Web3Auth";
|
import * as Web3Auth from "./auth/Web3Auth";
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
providerName: props.match.params.providerName,
|
providerName: props.match.params.providerName,
|
||||||
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||||
provider: null,
|
provider: null,
|
||||||
|
certs: [],
|
||||||
organizations: [],
|
organizations: [],
|
||||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||||
};
|
};
|
||||||
@ -47,6 +49,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this.getOrganizations();
|
this.getOrganizations();
|
||||||
this.getProvider();
|
this.getProvider();
|
||||||
|
this.getCerts(this.state.owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProvider() {
|
getProvider() {
|
||||||
@ -80,6 +83,17 @@ class ProviderEditPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCerts(owner) {
|
||||||
|
CertBackend.getCerts(owner)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === "ok") {
|
||||||
|
this.setState({
|
||||||
|
certs: res.data || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
parseProviderField(key, value) {
|
parseProviderField(key, value) {
|
||||||
if (["port"].includes(key)) {
|
if (["port"].includes(key)) {
|
||||||
value = Setting.myParseInt(value);
|
value = Setting.myParseInt(value);
|
||||||
@ -91,6 +105,11 @@ class ProviderEditPage extends React.Component {
|
|||||||
value = this.parseProviderField(key, value);
|
value = this.parseProviderField(key, value);
|
||||||
|
|
||||||
const provider = this.state.provider;
|
const provider = this.state.provider;
|
||||||
|
if (key === "owner" && provider["owner"] !== value) {
|
||||||
|
// the provider change the owner, reset the cert
|
||||||
|
provider["cert"] = "";
|
||||||
|
this.getCerts(value);
|
||||||
|
}
|
||||||
provider[key] = value;
|
provider[key] = value;
|
||||||
this.setState({
|
this.setState({
|
||||||
provider: provider,
|
provider: provider,
|
||||||
@ -1076,9 +1095,11 @@ class ProviderEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.provider.cert} onChange={e => {
|
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.cert} onChange={(value => {this.updateProviderField("cert", value);})}>
|
||||||
this.updateProviderField("cert", e.target.value);
|
{
|
||||||
}} />
|
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null
|
) : null
|
||||||
|
135
web/src/QrCodePage.js
Normal file
135
web/src/QrCodePage.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 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 QRCode from "qrcode.react";
|
||||||
|
import {Button, Col, Row} from "antd";
|
||||||
|
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||||
|
import * as Setting from "./Setting";
|
||||||
|
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
class QrCodePage extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
this.state = {
|
||||||
|
classes: props,
|
||||||
|
owner: props.owner ?? (props.match?.params?.owner ?? null),
|
||||||
|
paymentName: props.paymentName ?? (props.match?.params?.paymentName ?? null),
|
||||||
|
providerName: props.providerName ?? params.get("providerName"),
|
||||||
|
payUrl: props.payUrl ?? params.get("payUrl"),
|
||||||
|
successUrl: props.successUrl ?? params.get("successUrl"),
|
||||||
|
provider: props.provider ?? null,
|
||||||
|
payment: null,
|
||||||
|
timer: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProvider() {
|
||||||
|
if (!this.state.owner || !this.state.providerName) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await ProviderBackend.getProvider(this.state.owner, this.state.providerName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const provider = res.data;
|
||||||
|
this.setState({
|
||||||
|
provider: provider,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Setting.showMessage("error", err.message);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotifyTask() {
|
||||||
|
if (!this.state.owner || !this.state.paymentName) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyTask = async() => {
|
||||||
|
try {
|
||||||
|
const res = await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
throw new Error(res.msg);
|
||||||
|
}
|
||||||
|
const payment = res.data;
|
||||||
|
if (payment.state !== "Created") {
|
||||||
|
Setting.goToLink(this.state.successUrl);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Setting.showMessage("error", err.message);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
timer: setTimeout(async() => {
|
||||||
|
await notifyTask();
|
||||||
|
this.setNotifyTask();
|
||||||
|
}, 2000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.onUpdateApplication) {
|
||||||
|
this.props.onUpdateApplication(null);
|
||||||
|
}
|
||||||
|
this.getProvider();
|
||||||
|
this.setNotifyTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.state.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProviderInfo(provider) {
|
||||||
|
if (!provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = i18next.t(`product:${provider.type}`);
|
||||||
|
return (
|
||||||
|
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
|
||||||
|
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||||
|
} size={"large"} >
|
||||||
|
{
|
||||||
|
text
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.payUrl || !this.state.successUrl || !this.state.owner || !this.state.paymentName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="login-content">
|
||||||
|
<Col>
|
||||||
|
<Row style={{justifyContent: "center"}}>
|
||||||
|
{this.renderProviderInfo(this.state.provider)}
|
||||||
|
</Row>
|
||||||
|
<Row style={{marginTop: "10px", justifyContent: "center"}}>
|
||||||
|
<QRCode value={this.state.payUrl} size={this.props.size ?? 200} />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QrCodePage;
|
Loading…
x
Reference in New Issue
Block a user