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..df2f221 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 { + DEFAULT_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(DEFAULT_LANG_OPTION); const [environment, setEnvironment] = useState(DEFAULT_ENV); const [paymentService, setPaymentService] = useState( new PaymentService(DEFAULT_ENV, '', '') @@ -131,29 +136,51 @@ 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 === DEFAULT_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 +306,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..f8e0cf7 --- /dev/null +++ b/example/src/util/supportedLanguages.ts @@ -0,0 +1,18 @@ +export const DEFAULT_LANG_OPTION = 'default'; + +/** Lang values for PaymentSession.lang, matching iOS/Android SDK localizations. */ +export const SUPPORTED_LANG_OPTIONS = [ + DEFAULT_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;