diff --git a/object/product.go b/object/product.go index d27b0b1c..0e24cc46 100644 --- a/object/product.go +++ b/object/product.go @@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host ProductName: product.Name, PayerName: payerName, PayerId: user.Id, + PayerEmail: user.Email, PaymentName: paymentName, ProductDisplayName: product.DisplayName, + ProductDescription: product.Description, + ProductImage: product.Image, Price: product.Price, Currency: product.Currency, ReturnUrl: returnUrl, diff --git a/object/provider.go b/object/provider.go index d634b2a3..868ab0eb 100644 --- a/object/provider.go +++ b/object/provider.go @@ -325,6 +325,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) { return nil, err } 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" { pp, err := pp.NewBalancePaymentProvider() if err != nil { diff --git a/pp/airwallex.go b/pp/airwallex.go new file mode 100644 index 00000000..6a98ca3c --- /dev/null +++ b/pp/airwallex.go @@ -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 +} diff --git a/pp/provider.go b/pp/provider.go index f505d411..7d1b2602 100644 --- a/pp/provider.go +++ b/pp/provider.go @@ -33,8 +33,11 @@ type PayReq struct { ProductName string PayerName string PayerId string + PayerEmail string PaymentName string ProductDisplayName string + ProductDescription string + ProductImage string Price float64 Currency string diff --git a/web/src/PaymentResultPage.js b/web/src/PaymentResultPage.js index 4de000ec..7c8e9868 100644 --- a/web/src/PaymentResultPage.js +++ b/web/src/PaymentResultPage.js @@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component { payment: payment, }); 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({ 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 692bda1f..d8ded513 100644 --- a/web/src/ProductBuyPage.js +++ b/web/src/ProductBuyPage.js @@ -238,6 +238,8 @@ class ProductBuyPage extends React.Component { text = i18next.t("product:PayPal"); } else if (provider.type === "Stripe") { text = i18next.t("product:Stripe"); + } else if (provider.type === "AirWallex") { + text = i18next.t("product:AirWallex"); } return ( diff --git a/web/src/Setting.js b/web/src/Setting.js index 329ba47c..540317bd 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -279,6 +279,10 @@ export const OtherProviderInfo = { logo: `${StaticBaseUrl}/img/social_stripe.png`, url: "https://stripe.com/", }, + "AirWallex": { + logo: `${StaticBaseUrl}/img/payment_airwallex.svg`, + url: "https://airwallex.com/", + }, "GC": { logo: `${StaticBaseUrl}/img/payment_gc.png`, url: "https://gc.org", @@ -1106,6 +1110,7 @@ export function getProviderTypeOptions(category) { {id: "WeChat Pay", name: "WeChat Pay"}, {id: "PayPal", name: "PayPal"}, {id: "Stripe", name: "Stripe"}, + {id: "AirWallex", name: "AirWallex"}, {id: "GC", name: "GC"}, ]); } else if (category === "Captcha") { diff --git a/web/src/locales/ar/data.json b/web/src/locales/ar/data.json index fbf9cb4e..fafe0a27 100644 --- a/web/src/locales/ar/data.json +++ b/web/src/locales/ar/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/cs/data.json b/web/src/locales/cs/data.json index cb9e363e..bc25208f 100644 --- a/web/src/locales/cs/data.json +++ b/web/src/locales/cs/data.json @@ -757,6 +757,7 @@ "Sold": "Prodáno", "Sold - Tooltip": "Prodávané množství", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Štítek produktu", "Test buy page..": "Testovací stránka nákupu..", "There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.", diff --git a/web/src/locales/de/data.json b/web/src/locales/de/data.json index 73cd636a..18a56bc6 100644 --- a/web/src/locales/de/data.json +++ b/web/src/locales/de/data.json @@ -757,6 +757,7 @@ "Sold": "Verkauft", "Sold - Tooltip": "Menge verkauft", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag des Produkts", "Test buy page..": "Testkaufseite.", "There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.", diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index e17b5fb0..ebd04981 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/es/data.json b/web/src/locales/es/data.json index 9ee89462..88a8056e 100644 --- a/web/src/locales/es/data.json +++ b/web/src/locales/es/data.json @@ -757,6 +757,7 @@ "Sold": "Vendido", "Sold - Tooltip": "Cantidad vendida", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Etiqueta de producto", "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.", diff --git a/web/src/locales/fa/data.json b/web/src/locales/fa/data.json index b92ada06..7439f3d1 100644 --- a/web/src/locales/fa/data.json +++ b/web/src/locales/fa/data.json @@ -757,6 +757,7 @@ "Sold": "فروخته شده", "Sold - Tooltip": "تعداد فروخته شده", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "برچسب محصول", "Test buy page..": "صفحه تست خرید..", "There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.", diff --git a/web/src/locales/fi/data.json b/web/src/locales/fi/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/fi/data.json +++ b/web/src/locales/fi/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/fr/data.json b/web/src/locales/fr/data.json index 3c134e33..12816d07 100644 --- a/web/src/locales/fr/data.json +++ b/web/src/locales/fr/data.json @@ -757,6 +757,7 @@ "Sold": "Vendu", "Sold - Tooltip": "Quantité vendue", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Étiquette de produit", "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.", diff --git a/web/src/locales/he/data.json b/web/src/locales/he/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/he/data.json +++ b/web/src/locales/he/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/id/data.json b/web/src/locales/id/data.json index 0cf859d0..45d98e4e 100644 --- a/web/src/locales/id/data.json +++ b/web/src/locales/id/data.json @@ -757,6 +757,7 @@ "Sold": "Terjual", "Sold - Tooltip": "Jumlah terjual", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag produk", "Test buy page..": "Halaman pembelian uji coba.", "There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.", diff --git a/web/src/locales/it/data.json b/web/src/locales/it/data.json index a16fa734..1a7a4589 100644 --- a/web/src/locales/it/data.json +++ b/web/src/locales/it/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/ja/data.json b/web/src/locales/ja/data.json index 8a7fa49d..ef5a1af2 100644 --- a/web/src/locales/ja/data.json +++ b/web/src/locales/ja/data.json @@ -757,6 +757,7 @@ "Sold": "売れました", "Sold - Tooltip": "販売数量", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "製品のタグ", "Test buy page..": "テスト購入ページ。", "There is no payment channel for this product.": "この製品には支払いチャネルがありません。", diff --git a/web/src/locales/kk/data.json b/web/src/locales/kk/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/kk/data.json +++ b/web/src/locales/kk/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/ko/data.json b/web/src/locales/ko/data.json index 8bbb3edf..77808b18 100644 --- a/web/src/locales/ko/data.json +++ b/web/src/locales/ko/data.json @@ -757,6 +757,7 @@ "Sold": "팔렸습니다", "Sold - Tooltip": "판매량", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "제품 태그", "Test buy page..": "시험 구매 페이지.", "There is no payment channel for this product.": "이 제품에 대한 결제 채널이 없습니다.", diff --git a/web/src/locales/ms/data.json b/web/src/locales/ms/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/ms/data.json +++ b/web/src/locales/ms/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/nl/data.json b/web/src/locales/nl/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/nl/data.json +++ b/web/src/locales/nl/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/pl/data.json b/web/src/locales/pl/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/pl/data.json +++ b/web/src/locales/pl/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/pt/data.json b/web/src/locales/pt/data.json index 41b8bdee..1ccc3d84 100644 --- a/web/src/locales/pt/data.json +++ b/web/src/locales/pt/data.json @@ -757,6 +757,7 @@ "Sold": "Vendido", "Sold - Tooltip": "Quantidade vendida", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag do produto", "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.", diff --git a/web/src/locales/ru/data.json b/web/src/locales/ru/data.json index cacd93e0..df5c3776 100644 --- a/web/src/locales/ru/data.json +++ b/web/src/locales/ru/data.json @@ -757,6 +757,7 @@ "Sold": "Продано", "Sold - Tooltip": "Количество проданных", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Метка продукта", "Test buy page..": "Страница для тестовой покупки.", "There is no payment channel for this product.": "Для этого продукта нет канала оплаты.", diff --git a/web/src/locales/sk/data.json b/web/src/locales/sk/data.json index 3fc253db..1ac8ad20 100644 --- a/web/src/locales/sk/data.json +++ b/web/src/locales/sk/data.json @@ -757,6 +757,7 @@ "Sold": "Predané", "Sold - Tooltip": "Množstvo predaných kusov", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Štítok produktu", "Test buy page..": "Testovať stránku nákupu..", "There is no payment channel for this product.": "Pre tento produkt neexistuje platobný kanál.", diff --git a/web/src/locales/sv/data.json b/web/src/locales/sv/data.json index f1728b77..73740c1f 100644 --- a/web/src/locales/sv/data.json +++ b/web/src/locales/sv/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/tr/data.json b/web/src/locales/tr/data.json index 3f773f65..9c888746 100644 --- a/web/src/locales/tr/data.json +++ b/web/src/locales/tr/data.json @@ -757,6 +757,7 @@ "Sold": "Sold", "Sold - Tooltip": "Quantity sold", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Tag of product", "Test buy page..": "Test buy page..", "There is no payment channel for this product.": "There is no payment channel for this product.", diff --git a/web/src/locales/uk/data.json b/web/src/locales/uk/data.json index 2f0098cf..2adf18f7 100644 --- a/web/src/locales/uk/data.json +++ b/web/src/locales/uk/data.json @@ -757,6 +757,7 @@ "Sold": "Продано", "Sold - Tooltip": "Продана кількість", "Stripe": "смужка", + "AirWallex": "AirWallex", "Tag - Tooltip": "Тег товару", "Test buy page..": "Сторінка тестової покупки..", "There is no payment channel for this product.": "Для цього продукту немає платіжного каналу.", diff --git a/web/src/locales/vi/data.json b/web/src/locales/vi/data.json index 7b65ec03..b759294a 100644 --- a/web/src/locales/vi/data.json +++ b/web/src/locales/vi/data.json @@ -757,6 +757,7 @@ "Sold": "Đã bán", "Sold - Tooltip": "Số lượng bán ra", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "Nhãn sản phẩm", "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.", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index e7b4d185..7cd4c73f 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -757,6 +757,7 @@ "Sold": "售出", "Sold - Tooltip": "已售出的数量", "Stripe": "Stripe", + "AirWallex": "AirWallex", "Tag - Tooltip": "商品类别", "Test buy page..": "测试购买页面..", "There is no payment channel for this product.": "该商品没有付款方式。",