diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af20631..95c2b919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ # Changelog +## [6.0.11] - Login SDK - 2024-10-01 +### Fixed +- User registration via Xsolla Login Widget in WebView + +## [1.4.2] - Payments SDK - 2024-10-01 +### Changed +- `appendAnalytics(builder: Uri.Builder)` function. Added `browser_type` query parameter to URI that opens payment UI. Parameter is added for analytics purpose + +### Fixed +- Custom Tabs related crash for specific devices + ## [6.0.10] - Login SDK - 2024-09-11 ### Changed - `authenticateViaDeviceId` SDK method. Added `deviceId` optional parameter. diff --git a/build.gradle b/build.gradle index e87d5b98..6d452d8d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,10 @@ buildscript { - ext.payments_sdk_version_name = '1.4.1' + ext.payments_sdk_version_name = '1.4.2' ext.store_sdk_version_name = '2.5.10' ext.inventory_sdk_version_name = '2.0.4' - ext.login_sdk_version_name = '6.0.10' + ext.login_sdk_version_name = '6.0.11' ext.googleplay_sdk_version_name = "0.0.1" diff --git a/xsolla-login-sdk/src/main/java/com/xsolla/android/login/ui/ActivityAuthWebView.kt b/xsolla-login-sdk/src/main/java/com/xsolla/android/login/ui/ActivityAuthWebView.kt index efd9202d..5f1da096 100644 --- a/xsolla-login-sdk/src/main/java/com/xsolla/android/login/ui/ActivityAuthWebView.kt +++ b/xsolla-login-sdk/src/main/java/com/xsolla/android/login/ui/ActivityAuthWebView.kt @@ -57,6 +57,7 @@ internal class ActivityAuthWebView : ActivityAuth() { @SuppressLint("SetJavaScriptEnabled") private fun configureWebView() { webView.settings.javaScriptEnabled = true + webView.settings.domStorageEnabled = true val socialNetwork = intent.getSerializableExtra(ARG_SOCIAL_NETWORK) socialNetwork?.let { when (socialNetwork as SocialNetwork) { 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 f53fe959..61b313cc 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 @@ -18,6 +18,7 @@ import com.xsolla.android.payments.tracker.StatusTracker import com.xsolla.android.payments.ui.ActivityOrientationLock import com.xsolla.android.payments.ui.ActivityPayStation import com.xsolla.android.payments.ui.ActivityType +import com.xsolla.android.payments.ui.utils.BrowserUtils import com.xsolla.android.payments.ui.utils.TrustedWebActivityImageRef import com.xsolla.android.payments.util.AnalyticsUtils import kotlinx.parcelize.Parcelize @@ -256,6 +257,10 @@ class XPayments private constructor(private val statusTracker: StatusTracker, in ActivityPayStation.ARG_REDIRECT_SCHEME to redirectScheme, ActivityPayStation.ARG_REDIRECT_HOST to redirectHost ) + + Log.d(TAG, "generated PayStation url: $url") + Log.d(TAG, "received redirect host: $redirectHost and redirect scheme: $redirectScheme") + accessToken?.let { token -> bundle.putString(ActivityPayStation.ARG_PAYMENT_TOKEN, token.token) } @@ -294,6 +299,7 @@ class XPayments private constructor(private val statusTracker: StatusTracker, in } private fun generateUrl(): String { + accessToken?.let { val uriBuilder = Uri.Builder() .scheme("https") @@ -302,6 +308,7 @@ class XPayments private constructor(private val statusTracker: StatusTracker, in .appendQueryParameter(getTokenQueryParameterName(), it.token) appendAnalytics(uriBuilder) + return uriBuilder.build().toString() } throw IllegalArgumentException("access token isn't specified") @@ -310,10 +317,18 @@ class XPayments private constructor(private val statusTracker: StatusTracker, in private fun getServer() = if (isSandbox) SERVER_SANDBOX else SERVER_PROD private fun appendAnalytics(builder: Uri.Builder){ + + val browserType = when (BrowserUtils.deduceActivityType(context, activityType)) { + ActivityType.WEB_VIEW -> "web_view" + ActivityType.CUSTOM_TABS -> "custom_tabs" + ActivityType.TRUSTED_WEB_ACTIVITY -> "trusted_web_activity" + } + builder.appendQueryParameter("engine", "android") builder.appendQueryParameter("engine_v", Build.VERSION.RELEASE) builder.appendQueryParameter("sdk", AnalyticsUtils.sdk) builder.appendQueryParameter("sdk_v", AnalyticsUtils.sdkVersion) + builder.appendQueryParameter("browser_type", browserType) if (AnalyticsUtils.gameEngine.isNotBlank()) builder.appendQueryParameter("game_engine", AnalyticsUtils.gameEngine) 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 index c20719e1..043a5764 100644 --- 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 @@ -10,6 +10,7 @@ import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast +import androidx.annotation.MainThread import androidx.annotation.RequiresApi import androidx.browser.customtabs.CustomTabsSession import com.xsolla.android.payments.XPayments @@ -30,7 +31,7 @@ class PayStationCache(val context: Context) { XPayments.createIntentBuilder(context) var locale = Locale.getDefault().language if(locale.isEmpty()) locale = "en" - if(BrowserUtils.isCustomTabsBrowserAvailable(context)) { + if(BrowserUtils.getCustomTabsBrowserPackageName(context) != null) { val payStation3WarmUpUrl = "https://secure.xsolla.com/paystation3/$locale/cache-warmup" val payStation4WarmUpUrl = "https://secure.xsolla.com/paystation4/$locale/cache-warmup" customTabHelper = CustomTabsHelper( @@ -135,9 +136,10 @@ class PayStationCache(val context: Context) { companion object { private var instance: PayStationCache? = null + @MainThread fun getInstance(context: Context): PayStationCache { if (instance == null) { - instance = PayStationCache(context) + instance = PayStationCache(context.applicationContext).apply { init() } } return instance!! } 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 index 54e94407..2e579852 100644 --- 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 @@ -5,7 +5,7 @@ import androidx.startup.Initializer class PayStationCacheInitializer: Initializer { override fun create(context: Context): PayStationCache { - return PayStationCache.getInstance(context).apply { init() } + return PayStationCache.getInstance(context) } override fun dependencies(): List>> { 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 7982500c..e8c00084 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 @@ -84,9 +84,6 @@ internal class ActivityPayStation : AppCompatActivity() { */ private const val TRUSTED_WEB_ACTIVITY_FADE_OUT_TIME_IN_MILLIS = 250 - fun checkAvailability(context: Context) = - BrowserUtils.isPlainBrowserAvailable(context) - || BrowserUtils.isCustomTabsBrowserAvailable(context) } private lateinit var url: String @@ -97,7 +94,7 @@ internal class ActivityPayStation : AppCompatActivity() { private lateinit var redirectScheme: String private lateinit var redirectHost: String private var paymentToken: String? = null - private lateinit var type: ActivityType + private lateinit var activityType: ActivityType private var orientationLock: ActivityOrientationLock? = null private var trustedWebActivityBackgroundColor: Int? = null private var trustedWebActivityImageRef: TrustedWebActivityImageRef? = null @@ -128,17 +125,9 @@ internal class ActivityPayStation : AppCompatActivity() { paymentToken = token } - type = intent.getStringExtra(ARG_ACTIVITY_TYPE) - ?.let { s -> ActivityType.valueOf(s.uppercase()) } - // If activity type wasn't specified directly, fallback - // to the "deprecated" method, i.e. via [ARG_USE_WEBVIEW] - // intent parameter. - ?: if (BrowserUtils.isCustomTabsBrowserAvailable(this)) ActivityType.CUSTOM_TABS else ActivityType.WEB_VIEW + val receivedActivityType: ActivityType? = intent.getStringExtra(ARG_ACTIVITY_TYPE)?.let{ s -> ActivityType.valueOf(s.uppercase()) } ?: null - - if(!checkAvailability(this)) { - type = ActivityType.WEB_VIEW - } + activityType = BrowserUtils.deduceActivityType(this, receivedActivityType) orientationLock = intent.getStringExtra(ARG_ACTIVITY_ORIENTATION_LOCK) ?.let { s -> ActivityOrientationLock.valueOf(s.uppercase()) } @@ -379,8 +368,8 @@ internal class ActivityPayStation : AppCompatActivity() { dm.enqueue(request) } - private fun isWebView() : Boolean = type == ActivityType.WEB_VIEW - private fun isTrustedWebActivity() : Boolean = type == ActivityType.TRUSTED_WEB_ACTIVITY + private fun isWebView() : Boolean = activityType == ActivityType.WEB_VIEW + private fun isTrustedWebActivity() : Boolean = activityType == ActivityType.TRUSTED_WEB_ACTIVITY override fun onDestroy() { super.onDestroy() diff --git a/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/AsyncUtils.kt b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/AsyncUtils.kt new file mode 100644 index 00000000..90057051 --- /dev/null +++ b/xsolla-payments-sdk/src/main/java/com/xsolla/android/payments/ui/utils/AsyncUtils.kt @@ -0,0 +1,53 @@ +package com.xsolla.android.payments.ui.utils + +import android.os.Handler +import android.os.Looper +import android.util.Log +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +object AsyncUtils { + private val LOG_TAG = AsyncUtils.javaClass.simpleName + + private val sThreadFactory = InternalThreadFactory() + private val sThreadPool = Executors.newCachedThreadPool(sThreadFactory) + + private val sMainThreadExecutor = object : Executor { + private val mainHandler = Handler(Looper.getMainLooper()) + + override fun execute(r: Runnable) { + mainHandler.post(r) + } + } + + fun run(runnable: Runnable) : Future<*>? { + try { + return sThreadPool.submit(runnable) + } catch (e: Exception) { + Log.d(LOG_TAG, "Failed to execute a task asynchronously.", e) + return null + } + } + + fun runOnMainThread(runnable: Runnable) = + sMainThreadExecutor.execute(runnable) + + class InternalThreadFactory : ThreadFactory { + private val threadFactory = Executors.defaultThreadFactory() + private val nextThreadId = AtomicInteger(1) + + override fun newThread(r: Runnable?): Thread { + val thread = threadFactory.newThread(r) + val threadId = nextThreadId.getAndIncrement() + thread.name = "${THREAD_PREFIX}-${threadId}" + return thread; + } + + companion object { + private const val THREAD_PREFIX = "XsollaPayments" + } + } +} 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 e3480b96..67c84111 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 @@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat import com.google.androidbrowserhelper.trusted.ChromeLegacyUtils import com.xsolla.android.payments.R import com.xsolla.android.payments.caching.PayStationCache +import com.xsolla.android.payments.ui.ActivityType object BrowserUtils { @@ -52,8 +53,20 @@ object BrowserUtils { } } + /** + * Checks whether the system has a custom tabs compatible browser installed and + * a `CustomTabsSession` has been initialized. + */ fun isCustomTabsBrowserAvailable(context: Context) = - getAvailableCustomTabsBrowsers(context).isNotEmpty() && CustomTabsHelper.IS_SUCCESSFULLY_INITIALIZED + getCustomTabsBrowserPackageName(context) != null && + PayStationCache.getInstance(context).getCachedSession() != null + + /** + * Returns the package name of a browser that supports custom tabs or + * `null` if there's none available. + */ + fun getCustomTabsBrowserPackageName(context: Context) : String? = + getAvailableCustomTabsBrowsers(context).firstOrNull { !TextUtils.isEmpty(it) } fun isPlainBrowserAvailable(context: Context) = getAvailablePlainBrowsers(context).isNotEmpty() @@ -102,4 +115,13 @@ object BrowserUtils { activity.startActivity(intent) } + fun deduceActivityType(context: Context, preferredType: ActivityType?) : ActivityType { + var determinedType = preferredType ?: ActivityType.CUSTOM_TABS + + if (determinedType == ActivityType.TRUSTED_WEB_ACTIVITY && isTrustedWebActivityAvailable(context)) return ActivityType.TRUSTED_WEB_ACTIVITY + if (determinedType == ActivityType.CUSTOM_TABS && isCustomTabsBrowserAvailable(context)) return ActivityType.CUSTOM_TABS + + return ActivityType.WEB_VIEW + } + } \ No newline at end of file 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 index ee6013d6..091c7719 100644 --- 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 @@ -3,6 +3,8 @@ package com.xsolla.android.payments.ui.utils import android.content.ComponentName import android.content.Context import android.net.Uri +import android.util.Log +import androidx.annotation.MainThread import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession @@ -14,58 +16,102 @@ class CustomTabsHelper( private val onCustomTabsSessionCreated: (customTabsSession: CustomTabsSession) -> Unit ) { companion object { - var IS_SUCCESSFULLY_INITIALIZED: Boolean = true + private val LOG_TAG: String = CustomTabsHelper::class.java.simpleName } - private var customTabsSession: CustomTabsSession? = null + private var mClient: CustomTabsClient? = null + private var mCustomTabsSession: CustomTabsSession? = null + private var mBindingInProgress: Boolean = false - private var connection: CustomTabsServiceConnection? = object : CustomTabsServiceConnection() { + private var mConnection: CustomTabsServiceConnection = object : CustomTabsServiceConnection() { override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { - mClient = client - if(client != null) { - client.warmup(0) - customTabsSession = client.newSession(null) - customTabsSession!!.mayLaunchUrl(Uri.parse(payStation3WarmUpUrl), null, null) - customTabsSession!!.mayLaunchUrl(Uri.parse(payStation4WarmUpUrl), null, null) - onCustomTabsSessionCreated(customTabsSession!!) + Log.d(LOG_TAG, "onCustomTabsServiceConnected: '$name'") + + AsyncUtils.runOnMainThread { + if (mBindingInProgress) { + mBindingInProgress = false + + mClient = client + mClient!!.warmup(0) + + ensureCustomTabsSession() + } } - IS_SUCCESSFULLY_INITIALIZED = mClient != null } override fun onServiceDisconnected(name: ComponentName) { - mClient = null - customTabsSession = null - IS_SUCCESSFULLY_INITIALIZED = false + Log.d(LOG_TAG, "onServiceDisconnected: '$name'") + + AsyncUtils.runOnMainThread { + mClient = null + mCustomTabsSession = null + mBindingInProgress = false + } } } + @MainThread fun bindCustomTabsService() { - if (mClient != null) return - - val availableBrowsers = BrowserUtils.getAvailableCustomTabsBrowsers(context) + if (!mBindingInProgress && mClient == null) { + val browserPackageName = BrowserUtils.getCustomTabsBrowserPackageName(context) + if (browserPackageName != null) { + Log.d( + LOG_TAG, + "Attempting to bind to custom tabs service using the '$browserPackageName' browser." + ) - if(availableBrowsers.isEmpty()) return + mCustomTabsSession = null + mBindingInProgress = true - val packageName = BrowserUtils.getAvailableCustomTabsBrowsers(context).first() - - CustomTabsClient.bindCustomTabsService(context, packageName, connection!!) + CustomTabsClient.bindCustomTabsService(context, browserPackageName, mConnection) + } else { + Log.w( + LOG_TAG, + "No suitable browser could be found to start the custom tabs service binding process." + ) + } + } } + @MainThread fun unbindCustomTabsService() { - connection ?: return - context.unbindService(connection!!) - mClient = null - customTabsSession = null - connection = null + if (mBindingInProgress) { + mClient = null + mCustomTabsSession = null + mBindingInProgress = false + } else if (mClient != null) { + context.unbindService(mConnection) + } } - fun getSession(): CustomTabsSession? { - if (mClient == null) { - customTabsSession = null - } else if (customTabsSession == null) { - customTabsSession = mClient!!.newSession(null) + @MainThread + fun getSession(): CustomTabsSession? = ensureCustomTabsSession() + + /** + * Attempts to create a new [CustomTabsSession] if [mClient] is not `null` and [mCustomTabsSession] + * is `null`, otherwise returns current [mCustomTabsSession] value. + */ + @MainThread + private fun ensureCustomTabsSession() : CustomTabsSession? { + if (mClient != null && mCustomTabsSession == null) { + mCustomTabsSession = mClient!!.newSession(null) + if (mCustomTabsSession != null) { + mCustomTabsSession!!.mayLaunchUrl( + Uri.parse(payStation3WarmUpUrl), + null, + null + ) + + mCustomTabsSession!!.mayLaunchUrl( + Uri.parse(payStation4WarmUpUrl), + null, + null + ) + + onCustomTabsSessionCreated(mCustomTabsSession!!) + } } - return customTabsSession - } + return mCustomTabsSession + } } \ No newline at end of file