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:
Cutsin
2025-02-07 19:19:30 +08:00
committed by GitHub
parent 1a8cfe4ee6
commit b7a818e2d3
32 changed files with 334 additions and 1 deletions

View File

@ -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,

View File

@ -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
View 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&currency=%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
}

View File

@ -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

View File

@ -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);

View File

@ -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 (

View File

@ -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") {

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.": "برای این محصول کانال پرداختی وجود ندارد.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.": "この製品には支払いチャネルがありません。",

View File

@ -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.",

View File

@ -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.": "이 제품에 대한 결제 채널이 없습니다.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.": "Для этого продукта нет канала оплаты.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.": "Для цього продукту немає платіжного каналу.",

View File

@ -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.",

View File

@ -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.": "该商品没有付款方式。",