From a3f020d6cdaf46351aaea7a6193a3d4cb7db221b Mon Sep 17 00:00:00 2001 From: weiping-awx Date: Tue, 4 Aug 2026 17:20:26 +0800 Subject: [PATCH 1/2] feat: add lang to PaymentSession for payment UI locale Pass optional lang through iOS and Android session builders. Add an example app language picker for SDK-supported locales. Co-authored-by: Cursor --- .../util/AirwallexPaymentSessionConverter.kt | 2 + .../AirwallexRecurringSessionConverter.kt | 2 + ...llexRecurringWithIntentSessionConverter.kt | 2 + .../util/Converter+Extensions.kt | 4 ++ example/src/App.tsx | 61 +++++++++++++++---- example/src/util/SessionCreator.ts | 12 +++- example/src/util/supportedLanguages.ts | 18 ++++++ ios/AirwallexSdk+Session.swift | 3 + src/__tests__/index.test.tsx | 18 +++++- src/types/PaymentSession.ts | 2 + 10 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 example/src/util/supportedLanguages.ts diff --git a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexPaymentSessionConverter.kt b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexPaymentSessionConverter.kt index e973ec0..14d0703 100644 --- a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexPaymentSessionConverter.kt +++ b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexPaymentSessionConverter.kt @@ -33,6 +33,7 @@ object AirwallexPaymentSessionConverter { val currency = sessionMap.getStringOrNull("currency") ?: error("Currency is required") val countryCode = sessionMap.getStringOrNull("countryCode") ?: error("Country code is required") + val locale = sessionMap.toLocale() val paymentIntentId = sessionMap.getStringOrNull("paymentIntentId") ?: error("PaymentIntentId is required") @@ -60,6 +61,7 @@ object AirwallexPaymentSessionConverter { .setHidePaymentConsents(hidePaymentConsents) .setPaymentMethods(paymentMethods) .setShipping(shipping) + .setLocale(locale) .build() } diff --git a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringSessionConverter.kt b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringSessionConverter.kt index 6f67ed8..472cf0f 100644 --- a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringSessionConverter.kt +++ b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringSessionConverter.kt @@ -21,6 +21,7 @@ object AirwallexRecurringSessionConverter { val currency = sessionMap.getStringOrNull("currency") ?: error("currency is required") val countryCode = sessionMap.getStringOrNull("countryCode") ?: error("countryCode is required") + val locale = sessionMap.toLocale() val amount = sessionMap.getDoubleOrDefault("amount", -1.0).let { if (it == -1.0) error("amount is required") @@ -54,6 +55,7 @@ object AirwallexRecurringSessionConverter { .setReturnUrl(returnUrl) .setRequireEmail(isEmailRequired) .setPaymentMethods(paymentMethods) + .setLocale(locale) merchantTriggerReason?.let { sessionBuilder.setMerchantTriggerReason(merchantTriggerReason) diff --git a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringWithIntentSessionConverter.kt b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringWithIntentSessionConverter.kt index 01b5169..9d7a34c 100644 --- a/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringWithIntentSessionConverter.kt +++ b/android/src/main/java/com/airwallexpaymentreactnative/util/AirwallexRecurringWithIntentSessionConverter.kt @@ -24,6 +24,7 @@ object AirwallexRecurringWithIntentSessionConverter { val countryCode = sessionMap.getStringOrNull("countryCode") ?: error("countryCode is required") + val locale = sessionMap.toLocale() val amount = sessionMap.getDoubleOrDefault("amount", -1.0).let { if (it == -1.0) error("amount is required") @@ -70,6 +71,7 @@ object AirwallexRecurringWithIntentSessionConverter { .setReturnUrl(returnUrl) .setAutoCapture(autoCapture) .setShipping(shipping) + .setLocale(locale) merchantTriggerReason?.let { sessionBuilder.setMerchantTriggerReason(merchantTriggerReason) diff --git a/android/src/main/java/com/airwallexpaymentreactnative/util/Converter+Extensions.kt b/android/src/main/java/com/airwallexpaymentreactnative/util/Converter+Extensions.kt index ab8a567..6775e3e 100644 --- a/android/src/main/java/com/airwallexpaymentreactnative/util/Converter+Extensions.kt +++ b/android/src/main/java/com/airwallexpaymentreactnative/util/Converter+Extensions.kt @@ -7,6 +7,7 @@ import com.airwallex.android.core.model.PaymentMethod import com.airwallex.android.core.model.Shipping import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import java.util.Locale fun ReadableMap.getStringOrNull(key: String): String? = if (hasKey(key)) getString(key) else null @@ -26,6 +27,9 @@ fun ReadableMap.getArrayOrNull(key: String): ReadableArray? = fun ReadableMap.getMapOrNull(key: String): ReadableMap? = if (hasKey(key)) getMap(key) else null +fun ReadableMap.toLocale(): Locale? = + getStringOrNull("lang")?.let { Locale.forLanguageTag(it.replace('_', '-')) } + fun ReadableMap.toShipping(): Shipping? { val firstName = getStringOrNull("firstName") ?: getStringOrNull("first_name") val lastName = getStringOrNull("lastName") ?: getStringOrNull("last_name") diff --git a/example/src/App.tsx b/example/src/App.tsx index f65549c..1295af2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -32,6 +32,10 @@ import type { PaymentResult } from '../../src/types/PaymentResult'; import { useEffect, useState } from 'react'; import PaymentConsentCreator from './util/PaymentConsentCreator'; +import { + NULL_LANG_OPTION, + SUPPORTED_LANG_OPTIONS, +} from './util/supportedLanguages'; type Environment = 'staging' | 'demo' | 'preview'; type PaymentMode = 'oneOff' | 'recurring' | 'recurringAndPayment'; @@ -63,6 +67,7 @@ function Main() { const { showActionSheetWithOptions } = useActionSheet(); const [loading, setLoading] = useState(false); const [paymentMode, setPaymentMode] = useState('oneOff'); + const [selectedLang, setSelectedLang] = useState(NULL_LANG_OPTION); const [environment, setEnvironment] = useState(DEFAULT_ENV); const [paymentService, setPaymentService] = useState( new PaymentService(DEFAULT_ENV, '', '') @@ -131,29 +136,50 @@ function Main() { ); }; + const showLangSheet = () => { + const labels = [...SUPPORTED_LANG_OPTIONS, 'Cancel']; + showActionSheetWithOptions( + { + title: 'Language', + options: labels, + cancelButtonIndex: labels.length - 1, + }, + (selectedIndex) => { + if (selectedIndex == null || selectedIndex === labels.length - 1) + return; + setSelectedLang(SUPPORTED_LANG_OPTIONS[selectedIndex]!); + } + ); + }; + const paymentModeLabel = PAYMENT_MODE_OPTIONS.find((o) => o.value === paymentMode)?.label ?? 'Payment Mode'; async function fetchSession(requireCustomerId = false) { setLoading(true); + const lang = selectedLang === NULL_LANG_OPTION ? undefined : selectedLang; try { let session; switch (paymentMode) { case 'oneOff': session = await SessionCreator.createOneOffSession( paymentService, - requireCustomerId + requireCustomerId, + lang ); break; case 'recurring': - session = await SessionCreator.createRecurringSession(paymentService); + session = await SessionCreator.createRecurringSession( + paymentService, + lang + ); break; case 'recurringAndPayment': - session = - await SessionCreator.createRecurringWithIntentSession( - paymentService - ); + session = await SessionCreator.createRecurringWithIntentSession( + paymentService, + lang + ); break; default: throw new Error('Unknown payment mode'); @@ -279,9 +305,14 @@ function Main() { style={styles.loading} /> )} - - {paymentModeLabel} - + + + {paymentModeLabel} + + + lang: {selectedLang} + + { try { let customerId = null; @@ -110,6 +111,7 @@ class SessionCreator { autoCapture: true, hidePaymentConsents: false, clientSecret: clientSecret, + lang, }; return session; } catch (error) { @@ -119,7 +121,8 @@ class SessionCreator { } static async createRecurringSession( - paymentService: PaymentService + paymentService: PaymentService, + lang?: string ): Promise { try { const customerInfo = @@ -150,6 +153,7 @@ class SessionCreator { returnUrl: '', nextTriggeredBy: NextTriggeredBy.Merchant, merchantTriggerReason: MerchantTriggerReason.Scheduled, + lang, }; return session; } catch (error) { @@ -159,7 +163,8 @@ class SessionCreator { } static async createRecurringWithIntentSession( - paymentService: PaymentService + paymentService: PaymentService, + lang?: string ): Promise { try { const customerInfo = @@ -205,6 +210,7 @@ class SessionCreator { returnUrl: '', nextTriggeredBy: NextTriggeredBy.Merchant, merchantTriggerReason: MerchantTriggerReason.Scheduled, + lang, }; return session; } catch (error) { diff --git a/example/src/util/supportedLanguages.ts b/example/src/util/supportedLanguages.ts new file mode 100644 index 0000000..ee6a7fe --- /dev/null +++ b/example/src/util/supportedLanguages.ts @@ -0,0 +1,18 @@ +export const NULL_LANG_OPTION = 'null'; + +/** Lang values for PaymentSession.lang, matching iOS/Android SDK localizations. */ +export const SUPPORTED_LANG_OPTIONS = [ + NULL_LANG_OPTION, + 'de', + 'en', + 'es', + 'fr', + 'ja', + 'ko', + 'pt-BR', + 'pt-PT', + 'ru', + 'th', + 'zh-Hans', + 'zh-Hant', +]; diff --git a/ios/AirwallexSdk+Session.swift b/ios/AirwallexSdk+Session.swift index 27d9184..15dabfb 100644 --- a/ios/AirwallexSdk+Session.swift +++ b/ios/AirwallexSdk+Session.swift @@ -91,6 +91,9 @@ private extension AWXSession { } } countryCode = params["countryCode"] as! String + if let lang = params["lang"] as? String { + self.lang = lang + } if let returnUrl = params["returnUrl"] as? String { self.returnURL = returnUrl } diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index bf84291..ebf24e4 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -1 +1,17 @@ -it.todo('write a test'); +import type { OneOffSession } from '../types/PaymentSession'; + +describe('PaymentSession', () => { + it('includes lang when provided', () => { + const session: OneOffSession = { + type: 'OneOff', + clientSecret: 'testSecret', + currency: 'HKD', + countryCode: 'HK', + lang: 'zh-Hans', + amount: 50, + paymentIntentId: 'intent123', + }; + + expect(session.lang).toBe('zh-Hans'); + }); +}); diff --git a/src/types/PaymentSession.ts b/src/types/PaymentSession.ts index a7599b5..43287de 100644 --- a/src/types/PaymentSession.ts +++ b/src/types/PaymentSession.ts @@ -10,6 +10,8 @@ interface BaseSession { isEmailRequired?: boolean; currency: string; countryCode: string; + /** Payment UI locale (e.g. `zh-Hans`, `pt-BR`). Omit for SDK default. */ + lang?: string; amount: number; returnUrl?: string; googlePayOptions?: GooglePayOptions; From ee3e518ed3f37cdbd2306ac995ef5e2f66937cab Mon Sep 17 00:00:00 2001 From: weiping-awx Date: Wed, 26 Aug 2026 11:12:44 +0800 Subject: [PATCH 2/2] chore: use label `default` instead of `null` for default language option Co-authored-by: Cursor --- example/src/App.tsx | 7 ++++--- example/src/util/supportedLanguages.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 1295af2..df2f221 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -33,7 +33,7 @@ import { useEffect, useState } from 'react'; import PaymentConsentCreator from './util/PaymentConsentCreator'; import { - NULL_LANG_OPTION, + DEFAULT_LANG_OPTION, SUPPORTED_LANG_OPTIONS, } from './util/supportedLanguages'; @@ -67,7 +67,7 @@ function Main() { const { showActionSheetWithOptions } = useActionSheet(); const [loading, setLoading] = useState(false); const [paymentMode, setPaymentMode] = useState('oneOff'); - const [selectedLang, setSelectedLang] = useState(NULL_LANG_OPTION); + const [selectedLang, setSelectedLang] = useState(DEFAULT_LANG_OPTION); const [environment, setEnvironment] = useState(DEFAULT_ENV); const [paymentService, setPaymentService] = useState( new PaymentService(DEFAULT_ENV, '', '') @@ -158,7 +158,8 @@ function Main() { async function fetchSession(requireCustomerId = false) { setLoading(true); - const lang = selectedLang === NULL_LANG_OPTION ? undefined : selectedLang; + const lang = + selectedLang === DEFAULT_LANG_OPTION ? undefined : selectedLang; try { let session; switch (paymentMode) { diff --git a/example/src/util/supportedLanguages.ts b/example/src/util/supportedLanguages.ts index ee6a7fe..f8e0cf7 100644 --- a/example/src/util/supportedLanguages.ts +++ b/example/src/util/supportedLanguages.ts @@ -1,8 +1,8 @@ -export const NULL_LANG_OPTION = 'null'; +export const DEFAULT_LANG_OPTION = 'default'; /** Lang values for PaymentSession.lang, matching iOS/Android SDK localizations. */ export const SUPPORTED_LANG_OPTIONS = [ - NULL_LANG_OPTION, + DEFAULT_LANG_OPTION, 'de', 'en', 'es',