feat: support WeChat Pay via JSAPI (#2488)

* feat: support wechat jsapi payment

* feat: add log

* feat: update sign

* feat: process wechat pay result

* feat: process wechat pay result

* feat: save wechat openid for different app

* feat: save wechat openid for different app

* feat: add SetUserOAuthProperties for signup

* feat: fix openid for wechat

* feat: get user extra property in buyproduct

* feat: remove log

* feat: remove log

* feat: gofumpt code

* feat: change lr->crlf

* feat: change crlf->lf

* feat: improve code
This commit is contained in:
haiwu 2023-11-11 17:16:57 +08:00 committed by GitHub
parent d090e9c860
commit 0ac2b69f5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 324 additions and 97 deletions

View File

@ -547,7 +547,12 @@ func (c *ApiController) Login() {
if user.IsForbidden {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
// sync info from 3rd-party if possible
_, err := object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)

View File

@ -163,6 +163,8 @@ func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
@ -187,11 +189,11 @@ func (c *ApiController) BuyProduct() {
return
}
payment, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment)
c.ResponseOk(payment, attachInfo)
}

1
go.mod
View File

@ -32,6 +32,7 @@ require (
github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9

1
go.sum
View File

@ -1246,6 +1246,7 @@ github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo=
github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=

View File

@ -31,6 +31,7 @@ type UserInfo struct {
Phone string
CountryCode string
AvatarUrl string
Extra map[string]string
}
type ProviderInfo struct {

View File

@ -186,15 +186,24 @@ func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
id = wechatUserInfo.Openid
}
extra := make(map[string]string)
extra["wechat_unionid"] = wechatUserInfo.Openid
// For WeChat, different appId corresponds to different openId
extra[BuildWechatOpenIdKey(idp.Config.ClientID)] = wechatUserInfo.Openid
userInfo := UserInfo{
Id: id,
Username: wechatUserInfo.Nickname,
DisplayName: wechatUserInfo.Nickname,
AvatarUrl: wechatUserInfo.Headimgurl,
Extra: extra,
}
return &userInfo, nil
}
func BuildWechatOpenIdKey(appId string) string {
return fmt.Sprintf("wechat_openid_%s", appId)
}
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
request, err := http.NewRequest("GET", accessTokenUrl, nil)

View File

@ -17,6 +17,8 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
@ -158,30 +160,28 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string) (payment *Payment, attachInfo map[string]interface{}, err error) {
product, err := GetProduct(id)
if err != nil {
return nil, err
return nil, nil, err
}
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", id)
return nil, nil, fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerName)
if err != nil {
return nil, err
return nil, nil, err
}
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, err
return nil, nil, err
}
owner := product.Owner
productName := product.Name
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
@ -191,26 +191,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return nil, err
return nil, nil, err
}
if plan == nil {
return nil, fmt.Errorf("the plan: %s does not exist", planName)
return nil, nil, 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 nil, err
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
// Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
Price: product.Price,
Currency: product.Currency,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
// custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, err
return nil, nil, err
}
// Create a Payment linked with Product and Order
payment := &Payment{
payment = &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
@ -219,8 +239,8 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
Provider: provider.Name,
Type: provider.Type,
ProductName: productName,
ProductDisplayName: productDisplayName,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
@ -228,10 +248,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
ReturnUrl: product.ReturnUrl,
User: user.Name,
PayUrl: payUrl,
PayUrl: payResp.PayUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: orderId,
OutOrderId: payResp.OrderId,
}
if provider.Type == "Dummy" {
@ -240,13 +260,13 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
affected, err := AddPayment(payment)
if err != nil {
return nil, err
return nil, nil, err
}
if !affected {
return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
return payment, err
return payment, payResp.AttachInfo, nil
}
func ExtendProductWithProviders(product *Product) error {

View File

@ -20,6 +20,8 @@ import (
"reflect"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@ -142,6 +144,25 @@ func setUserProperty(user *User, field string, value string) {
}
}
func getUserProperty(user *User, field string) string {
if user.Properties == nil {
return ""
}
return user.Properties[field]
}
func getUserExtraProperty(user *User, providerType, key string) (string, error) {
extraJson := getUserProperty(user, fmt.Sprintf("oauth_%s_extra", providerType))
if extraJson == "" {
return "", nil
}
extra := make(map[string]string)
if err := jsoniter.Unmarshal([]byte(extraJson), &extra); err != nil {
return "", err
}
return extra[key], nil
}
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo) (bool, error) {
if userInfo.Id != "" {
propertyName := fmt.Sprintf("oauth_%s_id", providerType)
@ -185,6 +206,27 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
}
}
if userInfo.Extra != nil {
// Save extra info as json string
propertyName := fmt.Sprintf("oauth_%s_extra", providerType)
oldExtraJson := getUserProperty(user, propertyName)
extra := make(map[string]string)
if oldExtraJson != "" {
if err := jsoniter.Unmarshal([]byte(oldExtraJson), &extra); err != nil {
return false, err
}
}
for k, v := range userInfo.Extra {
extra[k] = v
}
newExtraJson, err := jsoniter.Marshal(extra)
if err != nil {
return false, err
}
setUserProperty(user, propertyName, string(newExtraJson))
}
return UpdateUserForAllFields(user.GetId(), user)
}

View File

@ -49,20 +49,24 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
return pp, nil
}
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *AlipayPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
pp.Client.SetReturnUrl(returnUrl)
pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", priceFloat64ToString(price))
pp.Client.SetReturnUrl(r.ReturnUrl)
pp.Client.SetNotifyUrl(r.NotifyUrl)
bm.Set("subject", joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName}))
bm.Set("out_trade_no", r.PaymentName)
bm.Set("total_amount", priceFloat64ToString(r.Price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
return payUrl, paymentName, nil
payResp := &PayResp{
PayUrl: payUrl,
OrderId: r.PaymentName,
}
return payResp, nil
}
func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -21,8 +21,10 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
return returnUrl, "", nil
func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
return &PayResp{
PayUrl: r.ReturnUrl,
}, nil
}
func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -153,22 +153,22 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *GcPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: paymentName,
Amount: getPriceString(price),
OrderNo: r.PaymentName,
Amount: getPriceString(r.Price),
Xmpch: pp.Xmpch,
Body: productDisplayName,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
Remark1: payerName,
Remark2: productName,
Body: r.ProductDisplayName,
ReturnUrl: r.ReturnUrl,
NotifyUrl: r.NotifyUrl,
Remark1: r.PayerName,
Remark2: r.ProductName,
}
b, err := json.Marshal(payReqInfo)
if err != nil {
return "", "", err
return nil, err
}
body := GcRequestBody{
@ -184,36 +184,38 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", "", err
return nil, err
}
respBytes, err := pp.doPost(bodyBytes)
if err != nil {
return "", "", err
return nil, err
}
var respBody GcResponseBody
err = json.Unmarshal(respBytes, &respBody)
if err != nil {
return "", "", err
return nil, err
}
if respBody.ReturnCode != "SUCCESS" {
return "", "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
return nil, fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", "", err
return nil, err
}
var payRespInfo GcPayRespInfo
err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
if err != nil {
return "", "", err
return nil, err
}
return payRespInfo.PayUrl, "", nil
payResp := &PayResp{
PayUrl: payRespInfo.PayUrl,
}
return payResp, nil
}
func (pp *GcPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -49,16 +49,16 @@ func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentPro
return pp, nil
}
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *PaypalPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// https://github.com/go-pay/gopay/blob/main/doc/paypal.md
units := make([]*paypal.PurchaseUnit, 0, 1)
unit := &paypal.PurchaseUnit{
ReferenceId: util.GetRandomString(16),
Amount: &paypal.Amount{
CurrencyCode: currency, // e.g."USD"
Value: priceFloat64ToString(price), // e.g."100.00"
CurrencyCode: r.Currency, // e.g."USD"
Value: priceFloat64ToString(r.Price), // e.g."100.00"
},
Description: joinAttachString([]string{productDisplayName, productName, providerName}),
Description: joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName}),
}
units = append(units, unit)
@ -68,23 +68,27 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
bm.SetBodyMap("application_context", func(b gopay.BodyMap) {
b.Set("brand_name", "Casdoor")
b.Set("locale", "en-PT")
b.Set("return_url", returnUrl)
b.Set("cancel_url", returnUrl)
b.Set("return_url", r.ReturnUrl)
b.Set("cancel_url", r.ReturnUrl)
})
ppRsp, err := pp.Client.CreateOrder(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
if ppRsp.Code != paypal.Success {
return "", "", errors.New(ppRsp.Error)
return nil, errors.New(ppRsp.Error)
}
// {"id":"9BR68863NE220374S","status":"CREATED",
// "links":[{"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"self","method":"GET"},
// {"href":"https://www.sandbox.paypal.com/checkoutnow?token=9BR68863NE220374S","rel":"approve","method":"GET"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"update","method":"PATCH"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S/capture","rel":"capture","method":"POST"}]}
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
payResp := &PayResp{
PayUrl: ppRsp.Response.Links[1].Href,
OrderId: ppRsp.Response.Id,
}
return payResp, nil
}
func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -24,6 +24,32 @@ const (
PaymentStateError PaymentState = "Error"
)
const (
PaymentEnvWechatBrowser = "WechatBrowser"
)
type PayReq struct {
ProviderName string
ProductName string
PayerName string
PayerId string
PaymentName string
ProductDisplayName string
Price float64
Currency string
ReturnUrl string
NotifyUrl string
PaymentEnv string
}
type PayResp struct {
PayUrl string
OrderId string
AttachInfo map[string]interface{}
}
type NotifyResult struct {
PaymentName string
PaymentStatus PaymentState
@ -39,7 +65,7 @@ type NotifyResult struct {
}
type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Pay(req *PayReq) (*PayResp, error)
Notify(body []byte, orderId string) (*NotifyResult, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string

View File

@ -46,30 +46,30 @@ func NewStripePaymentProvider(PublishableKey, SecretKey string) (*StripePaymentP
return pp, nil
}
func (pp *StripePaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (payUrl string, orderId string, err error) {
func (pp *StripePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// Create a temp product
description := joinAttachString([]string{productName, productDisplayName, providerName})
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
productParams := &stripe.ProductParams{
Name: stripe.String(productDisplayName),
Name: stripe.String(r.ProductDisplayName),
Description: stripe.String(description),
DefaultPriceData: &stripe.ProductDefaultPriceDataParams{
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Currency: stripe.String(r.Currency),
},
}
sProduct, err := stripeProduct.New(productParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a price for an existing product
priceParams := &stripe.PriceParams{
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(r.Currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Product: stripe.String(sProduct.ID),
}
sPrice, err := stripePrice.New(priceParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a Checkout Session
checkoutParams := &stripe.CheckoutSessionParams{
@ -80,17 +80,21 @@ func (pp *StripePaymentProvider) Pay(providerName string, productName string, pa
},
},
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(returnUrl),
CancelURL: stripe.String(returnUrl),
ClientReferenceID: stripe.String(paymentName),
SuccessURL: stripe.String(r.ReturnUrl),
CancelURL: stripe.String(r.ReturnUrl),
ClientReferenceID: stripe.String(r.PaymentName),
ExpiresAt: stripe.Int64(time.Now().Add(30 * time.Minute).Unix()),
}
checkoutParams.AddMetadata("product_description", description)
sCheckout, err := stripeCheckout.New(checkoutParams)
if err != nil {
return "", "", err
return nil, err
}
return sCheckout.URL, sCheckout.ID, nil
payResp := &PayResp{
PayUrl: sCheckout.URL,
OrderId: sCheckout.ID,
}
return payResp, nil
}
func (pp *StripePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -63,27 +63,66 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, seria
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(r *PayReq) (*PayResp, error) {
bm := gopay.BodyMap{}
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
desc := joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName})
bm.Set("attach", desc)
bm.Set("appid", pp.AppId)
bm.Set("description", productDisplayName)
bm.Set("notify_url", notifyUrl)
bm.Set("out_trade_no", paymentName)
bm.Set("description", r.ProductDisplayName)
bm.Set("notify_url", r.NotifyUrl)
bm.Set("out_trade_no", r.PaymentName)
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", priceFloat64ToInt64(price))
bm.Set("currency", currency)
bm.Set("total", priceFloat64ToInt64(r.Price))
bm.Set("currency", r.Currency)
})
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", "", err
// In Wechat browser, we use JSAPI
if r.PaymentEnv == PaymentEnvWechatBrowser {
if r.PayerId == "" {
return nil, errors.New("failed to get the payer's openid, please retry login")
}
bm.SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", r.PayerId) // If the account is signup via Wechat, the PayerId is the Wechat OpenId e.g.oxW9O1ZDvgreSHuBSQDiQ2F055PI
})
jsapiRsp, err := pp.Client.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return nil, err
}
if jsapiRsp.Code != wechat.Success {
return nil, errors.New(jsapiRsp.Error)
}
// use RSA256 to sign the pay request
params, err := pp.Client.PaySignOfJSAPI(pp.AppId, jsapiRsp.Response.PrepayId)
if err != nil {
return nil, err
}
payResp := &PayResp{
PayUrl: "",
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
AttachInfo: map[string]interface{}{
"appId": params.AppId,
"timeStamp": params.TimeStamp,
"nonceStr": params.NonceStr,
"package": params.Package,
"signType": "RSA",
"paySign": params.PaySign,
},
}
return payResp, nil
} else {
// In other case, we use NativeAPI
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return nil, err
}
if nativeRsp.Code != wechat.Success {
return nil, errors.New(nativeRsp.Error)
}
payResp := &PayResp{
PayUrl: nativeRsp.Response.CodeUrl,
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
}
return payResp, nil
}
if nativeRsp.Code != wechat.Success {
return "", "", errors.New(nativeRsp.Error)
}
return nativeRsp.Response.CodeUrl, paymentName, nil // Wechat can use paymentName as the OutTradeNo to query order status
}
func (pp *WechatPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -101,7 +101,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe", "Alipay"].includes(payment.type)) {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@ -31,6 +31,7 @@ class ProductBuyPage extends React.Component {
pricingName: props?.pricingName ?? props?.match?.params?.pricingName ?? null,
planName: params.get("plan"),
userName: params.get("user"),
paymentEnv: "",
product: null,
pricing: props?.pricing ?? null,
plan: null,
@ -38,8 +39,21 @@ class ProductBuyPage extends React.Component {
};
}
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,
});
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentEnv();
}
setStateAsync(state) {
@ -127,23 +141,74 @@ class ProductBuyPage extends React.Component {
return `${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`;
}
// 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);
}
}
buyProduct(product, provider) {
this.setState({
isPlacingOrder: true,
});
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 ?? "", this.state.paymentEnv)
.then((res) => {
if (res.status === "ok") {
const payment = res.data;
const attachInfo = res.data2;
let payUrl = payment.payUrl;
if (provider.type === "WeChat Pay") {
if (this.state.paymentEnv === "WechatBrowser") {
attachInfo.payment = payment;
this.callWechatPay(attachInfo);
return ;
}
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURI(payment.payUrl)}&successUrl=${encodeURI(payment.successUrl)}`;
}
Setting.goToLink(payUrl);
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.setState({
isPlacingOrder: false,
});
@ -218,7 +283,7 @@ class ProductBuyPage extends React.Component {
return (
<div className="login-content">
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
<Descriptions title={<span style={{fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 20} : {fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 25}}>
{Setting.getLanguageText(product?.displayName)}

View File

@ -70,8 +70,8 @@ export function deleteProduct(product) {
}).then(res => res.json());
}
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "") {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}`, {
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "") {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}`, {
method: "POST",
credentials: "include",
headers: {