diff --git a/CHANGELOG.md b/CHANGELOG.md
index 850fc1b1..30b3af40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,12 @@
# Changelog
+## [2.5.6] - Store SDK - 2024-05-06
+### Changed
+- `getVirtualItems` SDK method. Added the `requestGeoLocale` parameter. If `true`, the response returns the locale in the `geoLocale` parameter.
+
+## [1.3.4] - Payments SDK - 2024-05-06
+### Added
+- Pay Station preloader. Allows faster content display in WebView and Custom Tabs.
+
## [2.2.11] - Demo Apps - 2024-04-11
### Fixed
- Token refresh on application startup
diff --git a/build.gradle b/build.gradle
index 3f3f3d23..8a37f74c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,8 +2,8 @@
buildscript {
- ext.payments_sdk_version_name = '1.3.3'
- ext.store_sdk_version_name = '2.5.5'
+ ext.payments_sdk_version_name = '1.3.4'
+ ext.store_sdk_version_name = '2.5.6'
ext.inventory_sdk_version_name = '2.0.4'
ext.login_sdk_version_name = '6.0.7'
diff --git a/xsolla-payments-sdk/build.gradle b/xsolla-payments-sdk/build.gradle
index dee6824e..cb6590f0 100644
--- a/xsolla-payments-sdk/build.gradle
+++ b/xsolla-payments-sdk/build.gradle
@@ -41,6 +41,8 @@ dependencies {
implementation "androidx.core:core-ktx:$androidx_core_ktx"
implementation "androidx.browser:browser:$androidx_browser"
+
+ implementation "androidx.startup:startup-runtime:1.1.1"
}
dokkaHtmlPartial {
diff --git a/xsolla-payments-sdk/src/main/AndroidManifest.xml b/xsolla-payments-sdk/src/main/AndroidManifest.xml
index cb8476cc..3c5b583a 100644
--- a/xsolla-payments-sdk/src/main/AndroidManifest.xml
+++ b/xsolla-payments-sdk/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -30,6 +31,14 @@
android:scheme="app" />
+
+
+
\ No newline at end of file
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/XPayments.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/XPayments.kt
index bc24b29a..ab59b05d 100644
--- a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/XPayments.kt
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/XPayments.kt
@@ -5,8 +5,8 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Parcelable
-import android.util.Log
import androidx.core.os.bundleOf
+import com.xsolla.android.payments.caching.PayStationCache
import com.xsolla.android.payments.data.AccessToken
import com.xsolla.android.payments.ui.ActivityPayStation
import com.xsolla.android.payments.util.AnalyticsUtils
@@ -81,8 +81,7 @@ class XPayments {
*/
fun build(): Intent {
val url = generateUrl()
- val intent = Intent()
- intent.setClass(context, ActivityPayStation::class.java)
+ var intent = PayStationCache.getInstance(context).getCachedIntent()
intent.putExtras(
bundleOf(
ActivityPayStation.ARG_URL to url,
@@ -173,4 +172,5 @@ class XPayments {
V3,
V4
}
+
}
\ No newline at end of file
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCache.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCache.kt
new file mode 100644
index 00000000..491d08d2
--- /dev/null
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCache.kt
@@ -0,0 +1,141 @@
+package com.xsolla.android.payments.caching
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import android.view.ViewGroup
+import android.webkit.RenderProcessGoneDetail
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.browser.customtabs.CustomTabsSession
+import com.xsolla.android.payments.XPayments
+import com.xsolla.android.payments.ui.ActivityPayStation
+import com.xsolla.android.payments.ui.utils.BrowserUtils
+import com.xsolla.android.payments.ui.utils.CustomTabsHelper
+import java.util.Locale
+
+
+class PayStationCache(val context: Context) {
+
+ private lateinit var preloadingWebView:WebView
+ private var cachedIntent: Intent? = null
+ private lateinit var customTabHelper: CustomTabsHelper
+
+ fun init() {
+ XPayments.createIntentBuilder(context)
+ var locale = Locale.getDefault().language
+ if(locale.isEmpty()) locale = "en"
+ if(BrowserUtils.isCustomTabsBrowserAvailable(context)) {
+ val payStation3WarmUpUrl = "https://secure.xsolla.com/paystation3/$locale/cache-warmup"
+ val payStation4WarmUpUrl = "https://secure.xsolla.com/paystation4/$locale/cache-warmup"
+ customTabHelper = CustomTabsHelper(context, payStation3WarmUpUrl, payStation4WarmUpUrl)
+ customTabHelper.bindCustomTabsService()
+ } else {
+ preloadUrl("https://secure.xsolla.com/paystation4/$locale/cache-warmup")
+ }
+ }
+
+ fun getCachedIntent(): Intent {
+ if(cachedIntent == null) {
+ val newIntent = Intent()
+ newIntent.setClass(context, ActivityPayStation::class.java)
+ cachedIntent = newIntent
+ }
+ return cachedIntent!!
+ }
+
+ fun getCachedSession(): CustomTabsSession? {
+ return customTabHelper.getSession()
+ }
+
+ private fun preloadUrl(url: String) {
+ preloadingWebView = prepareWebView(context)
+ loadUrl(url, preloadingWebView)
+ }
+
+ private fun prepareWebView(context: Context): WebView {
+ val webView = WebView(context)
+ setupWebViewWithDefaults(webView)
+ return webView
+ }
+
+ private fun setupWebViewWithDefaults(webView: WebView) {
+ setWebViewSettings(webView)
+ setBrowserClients(webView)
+ }
+
+ private fun setWebViewSettings(webView: WebView?) {
+ requireNotNull(webView) { "WebView should not be null!" }
+ webView.settings.javaScriptEnabled = true
+ webView.settings.domStorageEnabled = true
+ webView.settings.allowFileAccess = true
+ webView.settings.loadsImagesAutomatically = true
+ }
+
+ private fun setBrowserClients(webView: WebView?) {
+ requireNotNull(webView) { "WebView should not be null!" }
+ try {
+ webView.webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(webview: WebView, url: String): Boolean {
+ Log.d(TAG, "shouldOverrideUrlLoading intercept url: $url")
+ webView.loadUrl(url)
+ return true
+ }
+
+ override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
+ val url = view.originalUrl
+ Toast.makeText(view.context, "Load failed with error: $description", Toast.LENGTH_LONG).show()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
+ if (!detail.didCrash()) {
+ Log.d(TAG, "System killed the WebView rendering process to reclaim memory. Recreating...")
+ if (view != null) {
+ val webViewContainer = view.parent as ViewGroup
+ if (webViewContainer != null && webViewContainer.childCount > 0) {
+ webViewContainer.removeView(view)
+ }
+ view.destroy()
+ }
+ return true
+ }
+ return false
+ }
+ }
+ webView.webChromeClient = object : WebChromeClient() {
+ override fun onProgressChanged(view: WebView, newProgress: Int) {
+ super.onProgressChanged(view, newProgress)
+ Log.d(TAG, "onProgressChanged: $newProgress")
+ if (view != null && newProgress == 100) {
+ val url = view.originalUrl
+ Log.d(TAG, "Preloading is done!")
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, e.message, e)
+ }
+ }
+
+ private fun loadUrl(url: String, webView: WebView) {
+ webView.loadUrl(url)
+ }
+
+ companion object {
+ private var instance: PayStationCache? = null
+
+ fun getInstance(context: Context): PayStationCache {
+ if(instance == null) {
+ instance = PayStationCache(context)
+ }
+ return instance!!
+ }
+
+ private const val TAG = "PayStationCache"
+ }
+}
\ No newline at end of file
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCacheInitializer.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCacheInitializer.kt
new file mode 100644
index 00000000..54e94407
--- /dev/null
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/caching/PayStationCacheInitializer.kt
@@ -0,0 +1,14 @@
+package com.xsolla.android.payments.caching
+
+import android.content.Context
+import androidx.startup.Initializer
+
+class PayStationCacheInitializer: Initializer {
+ override fun create(context: Context): PayStationCache {
+ return PayStationCache.getInstance(context).apply { init() }
+ }
+
+ override fun dependencies(): List>> {
+ return emptyList()
+ }
+}
\ No newline at end of file
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/ActivityPayStation.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/ActivityPayStation.kt
index 2032ac2e..84995a23 100644
--- a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/ActivityPayStation.kt
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/ActivityPayStation.kt
@@ -19,6 +19,7 @@ import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebView.WebViewTransport
import android.webkit.WebViewClient
+import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.xsolla.android.payments.R
@@ -41,6 +42,7 @@ internal class ActivityPayStation : AppCompatActivity() {
private lateinit var url: String
private lateinit var webView: WebView
private lateinit var childWebView: WebView
+ private lateinit var loader: FrameLayout
private lateinit var redirectScheme: String
private lateinit var redirectHost: String
@@ -82,6 +84,7 @@ internal class ActivityPayStation : AppCompatActivity() {
setContentView(R.layout.xsolla_payments_activity_paystation)
webView = findViewById(R.id.webview)
childWebView = findViewById(R.id.childWebView)
+ loader = findViewById(R.id.loader)
configureWebView()
webView.loadUrl(url)
} else {
@@ -193,7 +196,10 @@ internal class ActivityPayStation : AppCompatActivity() {
}
super.doUpdateVisitedHistory(view, url, isReload)
}
-
+ override fun onPageFinished(view: WebView?, url: String?) {
+ loader.visibility = View.GONE
+ super.onPageFinished(view, url)
+ }
}
webView.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ ->
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/BrowserUtils.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/BrowserUtils.kt
index 469114bf..7c4ab177 100644
--- a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/BrowserUtils.kt
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/BrowserUtils.kt
@@ -9,6 +9,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
import androidx.core.content.ContextCompat
import com.xsolla.android.payments.R
+import com.xsolla.android.payments.caching.PayStationCache
object BrowserUtils {
@@ -26,7 +27,7 @@ object BrowserUtils {
}
}
- private fun getAvailableCustomTabsBrowsers(context: Context): List {
+ fun getAvailableCustomTabsBrowsers(context: Context): List {
val browserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
@@ -66,14 +67,13 @@ object BrowserUtils {
)
.build()
- val intent = CustomTabsIntent.Builder()
+ val customTabsIntent = CustomTabsIntent.Builder(PayStationCache.getInstance(context).getCachedSession())
.setDefaultColorSchemeParams(colorSchemeParams)
.setShowTitle(true)
.setUrlBarHidingEnabled(true)
.build()
- intent.intent.`package` = getAvailableCustomTabsBrowsers(context).first()
-
- intent.launchUrl(context, Uri.parse(url))
+ customTabsIntent.intent.setPackage(getAvailableCustomTabsBrowsers(context).first())
+ customTabsIntent.launchUrl(context, Uri.parse(url))
}
fun launchPlainBrowser(activity: Activity, url: String) {
diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/CustomTabsHelper.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/CustomTabsHelper.kt
new file mode 100644
index 00000000..b9c62698
--- /dev/null
+++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/CustomTabsHelper.kt
@@ -0,0 +1,62 @@
+package com.xsolla.android.payments.ui.utils
+
+import android.content.ComponentName
+import android.content.Context
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsServiceConnection
+import androidx.browser.customtabs.CustomTabsSession
+
+class CustomTabsHelper(private val context: Context, private val payStation3WarmUpUrl: String, private val payStation4WarmUpUrl: String) {
+
+ private var customTabsSession: CustomTabsSession? = null
+ private var mClient: CustomTabsClient? = null
+
+ private var connection: CustomTabsServiceConnection? = object : CustomTabsServiceConnection() {
+ override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
+ if (client != null) {
+ mClient = client
+ client.warmup(0)
+ customTabsSession = client.newSession(null)
+ customTabsSession!!.mayLaunchUrl(Uri.parse(payStation3WarmUpUrl), null, null)
+ customTabsSession!!.mayLaunchUrl(Uri.parse(payStation4WarmUpUrl), null, null)
+
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ mClient = null
+ customTabsSession = null
+ }
+ }
+
+ fun bindCustomTabsService() {
+
+ if (mClient != null) return
+ val availableBrowsers = BrowserUtils.getAvailableCustomTabsBrowsers(context)
+
+ if(availableBrowsers.isEmpty()) return
+
+ val packageName = BrowserUtils.getAvailableCustomTabsBrowsers(context).first()
+
+ CustomTabsClient.bindCustomTabsService(context, packageName, connection!!)
+ }
+
+ fun unbindCustomTabsService() {
+ connection ?: return
+ context.unbindService(connection!!)
+ mClient = null
+ customTabsSession = null
+ connection = null
+ }
+
+ fun getSession(): CustomTabsSession? {
+ if (mClient == null) {
+ customTabsSession = null
+ } else if (customTabsSession == null) {
+ customTabsSession = mClient!!.newSession(null)
+ }
+ return customTabsSession
+ }
+
+}
\ No newline at end of file
diff --git a/xsolla-payments-sdk/src/main/res/layout/xsolla_payments_activity_paystation.xml b/xsolla-payments-sdk/src/main/res/layout/xsolla_payments_activity_paystation.xml
index e106af91..652a9d5a 100644
--- a/xsolla-payments-sdk/src/main/res/layout/xsolla_payments_activity_paystation.xml
+++ b/xsolla-payments-sdk/src/main/res/layout/xsolla_payments_activity_paystation.xml
@@ -15,4 +15,22 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
+
+
+
+
+
+
diff --git a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/XStore.kt b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/XStore.kt
index 1e86f128..f5e96450 100644
--- a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/XStore.kt
+++ b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/XStore.kt
@@ -36,6 +36,7 @@ import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
+import java.util.Locale
class XStore private constructor(
private val projectId: Int,
@@ -1143,6 +1144,8 @@ class XStore private constructor(
* The following languages are supported: Arabic (`ar`), Bulgarian (`bg`), Czech (`cs`), German (`de`), Spanish (`es`), French (`fr`), Hebrew (`he`), Italian (`it`), Japanese (`ja`), Korean (`ko`), Polish (`pl`), Portuguese (`pt`), Romanian (`ro`), Russian (`ru`), Thai (`th`), Turkish (`tr`), Vietnamese (`vi`), Chinese Simplified (`cn`), Chinese Traditional (`tw`), English (`en`, default).
* @param additionalFields The list of additional fields. Available fields: `media_list`, `order`, `long_description`.
* @param callback Status callback.
+ * @param requestGeoLocale If `true`, requests the locale based on user's current IP. The backend returns the locale as [VirtualItemsResponse.geoLocale].
+ * **IMPORTANT:** Setting [country] argument overrides the returned locale.
* @see [More about the use cases](https://developers.xsolla.com/sdk/android/catalog/catalog-display/).
*/
@JvmStatic
@@ -1153,16 +1156,19 @@ class XStore private constructor(
offset: Int = 0,
locale: String? = null,
additionalFields: List? = listOf(),
- country: String? = null
+ country: String? = null,
+ requestGeoLocale: Boolean? = null
) {
- getInstance().storeApi.getVirtualItems(
- getInstance().projectId,
- limit,
- offset,
- locale,
- additionalFields,
- country
- )
+ getInstance().storeApi
+ .getVirtualItems(
+ getInstance().projectId,
+ limit,
+ offset,
+ locale,
+ additionalFields,
+ country,
+ withGeo = requestGeoLocale
+ )
.enqueue(object : Callback {
override fun onResponse(
call: Call,
@@ -1171,7 +1177,32 @@ class XStore private constructor(
if (response.isSuccessful) {
val virtualItemsResponse = response.body()
if (virtualItemsResponse != null) {
- callback.onSuccess(virtualItemsResponse)
+ val geoLocale = requestGeoLocale?.takeIf { it }?.let {
+ val headers = response.headers()
+
+ // Retrieve language and country ISO codes.
+ val localeCode = headers["X-User-Locale-Code"]
+ val countryCode = headers["X-User-Country-Code"]
+
+ // If both ISO codes are present, only then attempt to parse those into a `Locale`.
+ if (!localeCode.isNullOrEmpty() && !countryCode.isNullOrEmpty()) {
+ try {
+ Locale(localeCode, countryCode)
+ } catch (_: NullPointerException) {
+ // We just ignore the locale altogether as it's not essential
+ // to the `VirtualItemsResponse`'s validity. Moreover, null
+ // pointer exception might only happen IF the code above is
+ // broken for some obscure reason, which shouldn't ever happen.
+ null
+ }
+ } else {
+ null
+ }
+ }
+
+ callback.onSuccess(
+ virtualItemsResponse.copy(geoLocale = geoLocale)
+ )
} else {
callback.onError(null, "Empty response")
}
diff --git a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/api/StoreApi.kt b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/api/StoreApi.kt
index aa052680..60152579 100644
--- a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/api/StoreApi.kt
+++ b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/api/StoreApi.kt
@@ -255,7 +255,8 @@ internal interface StoreApi {
@Query("offset") offset: Int,
@Query("locale") locale: String?,
@Query("additional_fields") additionalFields: List?,
- @Query("country") country: String?
+ @Query("country") country: String?,
+ @Query("with_geo") withGeo: Boolean?
): Call
@GET("/api/v2/project/{project_id}/items/virtual_items/all")
diff --git a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/entity/response/items/VirtualItemsResponse.kt b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/entity/response/items/VirtualItemsResponse.kt
index 56f0e7f5..dd6abe24 100644
--- a/xsolla-store-sdk/src/main/java/com/xsolla/android/store/entity/response/items/VirtualItemsResponse.kt
+++ b/xsolla-store-sdk/src/main/java/com/xsolla/android/store/entity/response/items/VirtualItemsResponse.kt
@@ -5,9 +5,20 @@ import com.google.gson.annotations.SerializedName
import com.xsolla.android.store.entity.response.common.*
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
+import java.util.Locale
-data class VirtualItemsResponse(@SerializedName("has_more") val hasMore: Boolean = false, val items: List- = emptyList()) {
+data class VirtualItemsResponse(
+ /**
+ * An optional locale returned by the backend based on user's current IP.
+ *
+ * Non-null only if request contains the `requestGeoLocale=true` query parameter.
+ */
+ @Transient val geoLocale: Locale? = null,
+ @SerializedName("has_more") val hasMore: Boolean = false,
+
+ val items: List
- = emptyList()
+) {
@Parcelize
data class Item(
val sku: String? = null,