diff --git a/controllers/auth.go b/controllers/auth.go index e11a5fb0..d1a028b5 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -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) diff --git a/controllers/product.go b/controllers/product.go index b2ed1fcc..55fb9cd0 100644 --- a/controllers/product.go +++ b/controllers/product.go @@ -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) } diff --git a/go.mod b/go.mod index c0d6ae1f..6ea8a1ae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 233baa39..5517e12a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/idp/provider.go b/idp/provider.go index fc7e437a..6f1b16da 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -31,6 +31,7 @@ type UserInfo struct { Phone string CountryCode string AvatarUrl string + Extra map[string]string } type ProviderInfo struct { diff --git a/idp/wechat.go b/idp/wechat.go index 9fef2c27..53a2ff45 100644 --- a/idp/wechat.go +++ b/idp/wechat.go @@ -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) diff --git a/object/product.go b/object/product.go index 78e41138..d26af73b 100644 --- a/object/product.go +++ b/object/product.go @@ -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 { diff --git a/object/user_util.go b/object/user_util.go index ed2d1c75..ed1df66c 100644 --- a/object/user_util.go +++ b/object/user_util.go @@ -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) } diff --git a/pp/alipay.go b/pp/alipay.go index 283129a5..873e560d 100644 --- a/pp/alipay.go +++ b/pp/alipay.go @@ -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) { diff --git a/pp/dummy.go b/pp/dummy.go index 041f616c..6a65a7de 100644 --- a/pp/dummy.go +++ b/pp/dummy.go @@ -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) { diff --git a/pp/gc.go b/pp/gc.go index eff18077..18c0918b 100644 --- a/pp/gc.go +++ b/pp/gc.go @@ -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) { diff --git a/pp/paypal.go b/pp/paypal.go index b3b41f22..c3059a02 100644 --- a/pp/paypal.go +++ b/pp/paypal.go @@ -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) { diff --git a/pp/provider.go b/pp/provider.go index 200bfe5f..f505d411 100644 --- a/pp/provider.go +++ b/pp/provider.go @@ -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 diff --git a/pp/stripe.go b/pp/stripe.go index 6acf73a5..9a0faf1e 100644 --- a/pp/stripe.go +++ b/pp/stripe.go @@ -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) { diff --git a/pp/wechatpay.go b/pp/wechatpay.go index a8fcc1e9..33d347eb 100644 --- a/pp/wechatpay.go +++ b/pp/wechatpay.go @@ -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) { diff --git a/web/src/PaymentResultPage.js b/web/src/PaymentResultPage.js index fd11aa69..cfb69381 100644 --- a/web/src/PaymentResultPage.js +++ b/web/src/PaymentResultPage.js @@ -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); diff --git a/web/src/ProductBuyPage.js b/web/src/ProductBuyPage.js index ba694da9..62e79522 100644 --- a/web/src/ProductBuyPage.js +++ b/web/src/ProductBuyPage.js @@ -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 (
- {i18next.t("product:Buy Product")}} bordered> + {i18next.t("product:Buy Product")}} bordered> {Setting.getLanguageText(product?.displayName)} diff --git a/web/src/backend/ProductBackend.js b/web/src/backend/ProductBackend.js index 1e090c31..940df7a6 100644 --- a/web/src/backend/ProductBackend.js +++ b/web/src/backend/ProductBackend.js @@ -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: {