mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-07 16:20:28 +08:00
feat: support AirWallex payment provider (#3558)
* feat: support AirWallex payment provider * chore: add some information due to AirWallex's risk control policy
This commit is contained in:
@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
ProductName: product.Name,
|
ProductName: product.Name,
|
||||||
PayerName: payerName,
|
PayerName: payerName,
|
||||||
PayerId: user.Id,
|
PayerId: user.Id,
|
||||||
|
PayerEmail: user.Email,
|
||||||
PaymentName: paymentName,
|
PaymentName: paymentName,
|
||||||
ProductDisplayName: product.DisplayName,
|
ProductDisplayName: product.DisplayName,
|
||||||
|
ProductDescription: product.Description,
|
||||||
|
ProductImage: product.Image,
|
||||||
Price: product.Price,
|
Price: product.Price,
|
||||||
Currency: product.Currency,
|
Currency: product.Currency,
|
||||||
ReturnUrl: returnUrl,
|
ReturnUrl: returnUrl,
|
||||||
|
@ -325,6 +325,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return pp, nil
|
return pp, nil
|
||||||
|
} else if typ == "AirWallex" {
|
||||||
|
pp, err := pp.NewAirwallexPaymentProvider(p.ClientId, p.ClientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
} else if typ == "Balance" {
|
} else if typ == "Balance" {
|
||||||
pp, err := pp.NewBalancePaymentProvider()
|
pp, err := pp.NewBalancePaymentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
289
pp/airwallex.go
Normal file
289
pp/airwallex.go
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
package pp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AirwallexPaymentProvider struct {
|
||||||
|
Client *AirwallexClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAirwallexPaymentProvider(clientId string, apiKey string) (*AirwallexPaymentProvider, error) {
|
||||||
|
isProd := conf.GetConfigString("runmode") == "prod"
|
||||||
|
apiEndpoint := "https://api-demo.airwallex.com/api/v1"
|
||||||
|
apiCheckout := "https://checkout-demo.airwallex.com/#/standalone/checkout?"
|
||||||
|
if isProd {
|
||||||
|
apiEndpoint = "https://api.airwallex.com/api/v1"
|
||||||
|
apiCheckout = "https://checkout.airwallex.com/#/standalone/checkout?"
|
||||||
|
}
|
||||||
|
client := &AirwallexClient{
|
||||||
|
ClientId: clientId,
|
||||||
|
APIKey: apiKey,
|
||||||
|
APIEndpoint: apiEndpoint,
|
||||||
|
APICheckout: apiCheckout,
|
||||||
|
client: &http.Client{Timeout: 15 * time.Second},
|
||||||
|
}
|
||||||
|
pp := &AirwallexPaymentProvider{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||||
|
// Create a payment intent
|
||||||
|
intent, err := pp.Client.CreateIntent(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payUrl, err := pp.Client.GetCheckoutUrl(intent, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PayResp{
|
||||||
|
PayUrl: payUrl,
|
||||||
|
OrderId: intent.MerchantOrderId,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||||
|
notifyResult := &NotifyResult{}
|
||||||
|
intent, err := pp.Client.GetIntentByOrderId(orderId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Check intent status
|
||||||
|
switch intent.Status {
|
||||||
|
case "PENDING", "REQUIRES_PAYMENT_METHOD", "REQUIRES_CUSTOMER_ACTION", "REQUIRES_CAPTURE":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCreated
|
||||||
|
return notifyResult, nil
|
||||||
|
case "CANCELLED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||||
|
return notifyResult, nil
|
||||||
|
case "EXPIRED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateTimeout
|
||||||
|
return notifyResult, nil
|
||||||
|
case "SUCCEEDED":
|
||||||
|
// Skip
|
||||||
|
default:
|
||||||
|
notifyResult.PaymentStatus = PaymentStateError
|
||||||
|
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout status: %v", intent.Status)
|
||||||
|
return notifyResult, nil
|
||||||
|
}
|
||||||
|
// Check attempt status
|
||||||
|
if intent.PaymentStatus != "" {
|
||||||
|
switch intent.PaymentStatus {
|
||||||
|
case "CANCELLED", "EXPIRED", "RECEIVED", "AUTHENTICATION_REDIRECTED", "AUTHORIZED", "CAPTURE_REQUESTED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCreated
|
||||||
|
return notifyResult, nil
|
||||||
|
case "PAID", "SETTLED":
|
||||||
|
// Skip
|
||||||
|
default:
|
||||||
|
notifyResult.PaymentStatus = PaymentStateError
|
||||||
|
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout payment status: %v", intent.PaymentStatus)
|
||||||
|
return notifyResult, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The Payment has succeeded.
|
||||||
|
var productDisplayName, productName, providerName string
|
||||||
|
if description, ok := intent.Metadata["description"]; ok {
|
||||||
|
productName, productDisplayName, providerName, _ = parseAttachString(description.(string))
|
||||||
|
}
|
||||||
|
orderId = intent.MerchantOrderId
|
||||||
|
return &NotifyResult{
|
||||||
|
PaymentName: orderId,
|
||||||
|
PaymentStatus: PaymentStatePaid,
|
||||||
|
ProductName: productName,
|
||||||
|
ProductDisplayName: productDisplayName,
|
||||||
|
ProviderName: providerName,
|
||||||
|
Price: priceStringToFloat64(intent.Amount.String()),
|
||||||
|
Currency: intent.Currency,
|
||||||
|
OrderId: orderId,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) GetInvoice(paymentName, personName, personIdCard, personEmail, personPhone, invoiceType, invoiceTitle, invoiceTaxId string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) GetResponseError(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
return "fail"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Airwallex Client implementation (to be removed upon official SDK release)
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AirwallexClient struct {
|
||||||
|
ClientId string
|
||||||
|
APIKey string
|
||||||
|
APIEndpoint string
|
||||||
|
APICheckout string
|
||||||
|
client *http.Client
|
||||||
|
tokenCache *AirWallexTokenInfo
|
||||||
|
tokenMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexTokenInfo struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
parsedExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexIntentResp struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
MerchantOrderId string `json:"merchant_order_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetToken() (string, error) {
|
||||||
|
c.tokenMutex.Lock()
|
||||||
|
defer c.tokenMutex.Unlock()
|
||||||
|
if c.tokenCache != nil && time.Now().Before(c.tokenCache.parsedExpiresAt) {
|
||||||
|
return c.tokenCache.Token, nil
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest("POST", c.APIEndpoint+"/authentication/login", bytes.NewBuffer([]byte("{}")))
|
||||||
|
req.Header.Set("x-client-id", c.ClientId)
|
||||||
|
req.Header.Set("x-api-key", c.APIKey)
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result AirWallexTokenInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result.Token == "" {
|
||||||
|
return "", fmt.Errorf("invalid token response")
|
||||||
|
}
|
||||||
|
expiresAt := strings.Replace(result.ExpiresAt, "+0000", "+00:00", 1)
|
||||||
|
result.parsedExpiresAt, _ = time.Parse(time.RFC3339, expiresAt)
|
||||||
|
c.tokenCache = &result
|
||||||
|
return result.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map[string]interface{}, error) {
|
||||||
|
token, err := c.GetToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(body)
|
||||||
|
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) CreateIntent(r *PayReq) (*AirWallexIntentResp, error) {
|
||||||
|
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||||
|
orderId := r.PaymentName
|
||||||
|
intentReq := map[string]interface{}{
|
||||||
|
"currency": r.Currency,
|
||||||
|
"amount": r.Price,
|
||||||
|
"merchant_order_id": orderId,
|
||||||
|
"request_id": orderId,
|
||||||
|
"descriptor": string([]rune(description)[:32]), // display to the customer.
|
||||||
|
"metadata": map[string]interface{}{"description": description},
|
||||||
|
"order": map[string]interface{}{"products": []map[string]interface{}{{"name": r.ProductDisplayName, "quantity": 1, "desc": r.ProductDescription, "image_url": r.ProductImage}}},
|
||||||
|
"customer": map[string]interface{}{"merchant_customer_id": r.PayerId, "email": r.PayerEmail, "first_name": r.PayerName, "last_name": r.PayerName},
|
||||||
|
}
|
||||||
|
intentUrl := fmt.Sprintf("%s/pa/payment_intents/create", c.APIEndpoint)
|
||||||
|
intentRes, err := c.authRequest("POST", intentUrl, intentReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create payment intent: %v", err)
|
||||||
|
}
|
||||||
|
return &AirWallexIntentResp{
|
||||||
|
Id: intentRes["id"].(string),
|
||||||
|
ClientSecret: intentRes["client_secret"].(string),
|
||||||
|
MerchantOrderId: intentRes["merchant_order_id"].(string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirwallexIntent struct {
|
||||||
|
Amount json.Number `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Descriptor string `json:"descriptor"`
|
||||||
|
MerchantOrderId string `json:"merchant_order_id"`
|
||||||
|
LatestPaymentAttempt struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"latest_payment_attempt"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirwallexIntents struct {
|
||||||
|
Items []AirwallexIntent `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexIntentInfo struct {
|
||||||
|
Amount json.Number
|
||||||
|
Currency string
|
||||||
|
Id string
|
||||||
|
Status string
|
||||||
|
Descriptor string
|
||||||
|
MerchantOrderId string
|
||||||
|
PaymentStatus string
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetIntentByOrderId(orderId string) (*AirWallexIntentInfo, error) {
|
||||||
|
intentUrl := fmt.Sprintf("%s/pa/payment_intents/?merchant_order_id=%s", c.APIEndpoint, orderId)
|
||||||
|
intentRes, err := c.authRequest("GET", intentUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get payment intent: %v", err)
|
||||||
|
}
|
||||||
|
items := intentRes["items"].([]interface{})
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, fmt.Errorf("no payment intent found for order id: %s", orderId)
|
||||||
|
}
|
||||||
|
var intent AirwallexIntent
|
||||||
|
if b, err := json.Marshal(items[0]); err == nil {
|
||||||
|
json.Unmarshal(b, &intent)
|
||||||
|
}
|
||||||
|
return &AirWallexIntentInfo{
|
||||||
|
Id: intent.Id,
|
||||||
|
Amount: intent.Amount,
|
||||||
|
Currency: intent.Currency,
|
||||||
|
Status: intent.Status,
|
||||||
|
Descriptor: intent.Descriptor,
|
||||||
|
MerchantOrderId: intent.MerchantOrderId,
|
||||||
|
PaymentStatus: intent.LatestPaymentAttempt.Status,
|
||||||
|
Metadata: intent.Metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetCheckoutUrl(intent *AirWallexIntentResp, r *PayReq) (string, error) {
|
||||||
|
return fmt.Sprintf("%sintent_id=%s&client_secret=%s&mode=payment¤cy=%s&amount=%v&requiredBillingContactFields=%s&successUrl=%s&failUrl=%s&logoUrl=%s",
|
||||||
|
c.APICheckout,
|
||||||
|
intent.Id,
|
||||||
|
intent.ClientSecret,
|
||||||
|
r.Currency,
|
||||||
|
r.Price,
|
||||||
|
url.QueryEscape(`["address"]`),
|
||||||
|
r.ReturnUrl,
|
||||||
|
r.ReturnUrl,
|
||||||
|
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", // replace default logo
|
||||||
|
), nil
|
||||||
|
}
|
@ -33,8 +33,11 @@ type PayReq struct {
|
|||||||
ProductName string
|
ProductName string
|
||||||
PayerName string
|
PayerName string
|
||||||
PayerId string
|
PayerId string
|
||||||
|
PayerEmail string
|
||||||
PaymentName string
|
PaymentName string
|
||||||
ProductDisplayName string
|
ProductDisplayName string
|
||||||
|
ProductDescription string
|
||||||
|
ProductImage string
|
||||||
Price float64
|
Price float64
|
||||||
Currency string
|
Currency string
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
payment: payment,
|
payment: payment,
|
||||||
});
|
});
|
||||||
if (payment.state === "Created") {
|
if (payment.state === "Created") {
|
||||||
if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
timeout: setTimeout(async() => {
|
timeout: setTimeout(async() => {
|
||||||
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||||
|
@ -238,6 +238,8 @@ class ProductBuyPage extends React.Component {
|
|||||||
text = i18next.t("product:PayPal");
|
text = i18next.t("product:PayPal");
|
||||||
} else if (provider.type === "Stripe") {
|
} else if (provider.type === "Stripe") {
|
||||||
text = i18next.t("product:Stripe");
|
text = i18next.t("product:Stripe");
|
||||||
|
} else if (provider.type === "AirWallex") {
|
||||||
|
text = i18next.t("product:AirWallex");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -279,6 +279,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
||||||
url: "https://stripe.com/",
|
url: "https://stripe.com/",
|
||||||
},
|
},
|
||||||
|
"AirWallex": {
|
||||||
|
logo: `${StaticBaseUrl}/img/payment_airwallex.svg`,
|
||||||
|
url: "https://airwallex.com/",
|
||||||
|
},
|
||||||
"GC": {
|
"GC": {
|
||||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||||
url: "https://gc.org",
|
url: "https://gc.org",
|
||||||
@ -1106,6 +1110,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "WeChat Pay", name: "WeChat Pay"},
|
{id: "WeChat Pay", name: "WeChat Pay"},
|
||||||
{id: "PayPal", name: "PayPal"},
|
{id: "PayPal", name: "PayPal"},
|
||||||
{id: "Stripe", name: "Stripe"},
|
{id: "Stripe", name: "Stripe"},
|
||||||
|
{id: "AirWallex", name: "AirWallex"},
|
||||||
{id: "GC", name: "GC"},
|
{id: "GC", name: "GC"},
|
||||||
]);
|
]);
|
||||||
} else if (category === "Captcha") {
|
} else if (category === "Captcha") {
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Prodáno",
|
"Sold": "Prodáno",
|
||||||
"Sold - Tooltip": "Prodávané množství",
|
"Sold - Tooltip": "Prodávané množství",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Štítek produktu",
|
"Tag - Tooltip": "Štítek produktu",
|
||||||
"Test buy page..": "Testovací stránka nákupu..",
|
"Test buy page..": "Testovací stránka nákupu..",
|
||||||
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
|
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Verkauft",
|
"Sold": "Verkauft",
|
||||||
"Sold - Tooltip": "Menge verkauft",
|
"Sold - Tooltip": "Menge verkauft",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag des Produkts",
|
"Tag - Tooltip": "Tag des Produkts",
|
||||||
"Test buy page..": "Testkaufseite.",
|
"Test buy page..": "Testkaufseite.",
|
||||||
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
|
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendido",
|
"Sold": "Vendido",
|
||||||
"Sold - Tooltip": "Cantidad vendida",
|
"Sold - Tooltip": "Cantidad vendida",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Etiqueta de producto",
|
"Tag - Tooltip": "Etiqueta de producto",
|
||||||
"Test buy page..": "Página de compra de prueba.",
|
"Test buy page..": "Página de compra de prueba.",
|
||||||
"There is no payment channel for this product.": "No hay canal de pago para este producto.",
|
"There is no payment channel for this product.": "No hay canal de pago para este producto.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "فروخته شده",
|
"Sold": "فروخته شده",
|
||||||
"Sold - Tooltip": "تعداد فروخته شده",
|
"Sold - Tooltip": "تعداد فروخته شده",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "برچسب محصول",
|
"Tag - Tooltip": "برچسب محصول",
|
||||||
"Test buy page..": "صفحه تست خرید..",
|
"Test buy page..": "صفحه تست خرید..",
|
||||||
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
|
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendu",
|
"Sold": "Vendu",
|
||||||
"Sold - Tooltip": "Quantité vendue",
|
"Sold - Tooltip": "Quantité vendue",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Étiquette de produit",
|
"Tag - Tooltip": "Étiquette de produit",
|
||||||
"Test buy page..": "Page d'achat de test.",
|
"Test buy page..": "Page d'achat de test.",
|
||||||
"There is no payment channel for this product.": "Il n'y a aucun canal de paiement pour ce produit.",
|
"There is no payment channel for this product.": "Il n'y a aucun canal de paiement pour ce produit.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Terjual",
|
"Sold": "Terjual",
|
||||||
"Sold - Tooltip": "Jumlah terjual",
|
"Sold - Tooltip": "Jumlah terjual",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag produk",
|
"Tag - Tooltip": "Tag produk",
|
||||||
"Test buy page..": "Halaman pembelian uji coba.",
|
"Test buy page..": "Halaman pembelian uji coba.",
|
||||||
"There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.",
|
"There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "売れました",
|
"Sold": "売れました",
|
||||||
"Sold - Tooltip": "販売数量",
|
"Sold - Tooltip": "販売数量",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "製品のタグ",
|
"Tag - Tooltip": "製品のタグ",
|
||||||
"Test buy page..": "テスト購入ページ。",
|
"Test buy page..": "テスト購入ページ。",
|
||||||
"There is no payment channel for this product.": "この製品には支払いチャネルがありません。",
|
"There is no payment channel for this product.": "この製品には支払いチャネルがありません。",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "팔렸습니다",
|
"Sold": "팔렸습니다",
|
||||||
"Sold - Tooltip": "판매량",
|
"Sold - Tooltip": "판매량",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "제품 태그",
|
"Tag - Tooltip": "제품 태그",
|
||||||
"Test buy page..": "시험 구매 페이지.",
|
"Test buy page..": "시험 구매 페이지.",
|
||||||
"There is no payment channel for this product.": "이 제품에 대한 결제 채널이 없습니다.",
|
"There is no payment channel for this product.": "이 제품에 대한 결제 채널이 없습니다.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendido",
|
"Sold": "Vendido",
|
||||||
"Sold - Tooltip": "Quantidade vendida",
|
"Sold - Tooltip": "Quantidade vendida",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag do produto",
|
"Tag - Tooltip": "Tag do produto",
|
||||||
"Test buy page..": "Página de teste de compra...",
|
"Test buy page..": "Página de teste de compra...",
|
||||||
"There is no payment channel for this product.": "Não há canal de pagamento disponível para este produto.",
|
"There is no payment channel for this product.": "Não há canal de pagamento disponível para este produto.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Продано",
|
"Sold": "Продано",
|
||||||
"Sold - Tooltip": "Количество проданных",
|
"Sold - Tooltip": "Количество проданных",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Метка продукта",
|
"Tag - Tooltip": "Метка продукта",
|
||||||
"Test buy page..": "Страница для тестовой покупки.",
|
"Test buy page..": "Страница для тестовой покупки.",
|
||||||
"There is no payment channel for this product.": "Для этого продукта нет канала оплаты.",
|
"There is no payment channel for this product.": "Для этого продукта нет канала оплаты.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Predané",
|
"Sold": "Predané",
|
||||||
"Sold - Tooltip": "Množstvo predaných kusov",
|
"Sold - Tooltip": "Množstvo predaných kusov",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Štítok produktu",
|
"Tag - Tooltip": "Štítok produktu",
|
||||||
"Test buy page..": "Testovať stránku nákupu..",
|
"Test buy page..": "Testovať stránku nákupu..",
|
||||||
"There is no payment channel for this product.": "Pre tento produkt neexistuje platobný kanál.",
|
"There is no payment channel for this product.": "Pre tento produkt neexistuje platobný kanál.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Продано",
|
"Sold": "Продано",
|
||||||
"Sold - Tooltip": "Продана кількість",
|
"Sold - Tooltip": "Продана кількість",
|
||||||
"Stripe": "смужка",
|
"Stripe": "смужка",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Тег товару",
|
"Tag - Tooltip": "Тег товару",
|
||||||
"Test buy page..": "Сторінка тестової покупки..",
|
"Test buy page..": "Сторінка тестової покупки..",
|
||||||
"There is no payment channel for this product.": "Для цього продукту немає платіжного каналу.",
|
"There is no payment channel for this product.": "Для цього продукту немає платіжного каналу.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Đã bán",
|
"Sold": "Đã bán",
|
||||||
"Sold - Tooltip": "Số lượng bán ra",
|
"Sold - Tooltip": "Số lượng bán ra",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Nhãn sản phẩm",
|
"Tag - Tooltip": "Nhãn sản phẩm",
|
||||||
"Test buy page..": "Trang mua thử.",
|
"Test buy page..": "Trang mua thử.",
|
||||||
"There is no payment channel for this product.": "Không có kênh thanh toán cho sản phẩm này.",
|
"There is no payment channel for this product.": "Không có kênh thanh toán cho sản phẩm này.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "售出",
|
"Sold": "售出",
|
||||||
"Sold - Tooltip": "已售出的数量",
|
"Sold - Tooltip": "已售出的数量",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "商品类别",
|
"Tag - Tooltip": "商品类别",
|
||||||
"Test buy page..": "测试购买页面..",
|
"Test buy page..": "测试购买页面..",
|
||||||
"There is no payment channel for this product.": "该商品没有付款方式。",
|
"There is no payment channel for this product.": "该商品没有付款方式。",
|
||||||
|
Reference in New Issue
Block a user