diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
index 173445bca..57fcb733d 100644
--- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
+++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
@@ -7,9 +7,10 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Color
import android.os.Build
import android.view.MenuItem
+import android.view.View
import android.view.View.INVISIBLE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.Window
import android.view.WindowManager
import android.widget.ProgressBar
import android.widget.RelativeLayout
@@ -17,10 +18,14 @@ import androidx.activity.ComponentActivity
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toDrawable
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.children
import com.shopify.checkoutkit.ShopifyCheckoutKit.log
@@ -32,6 +37,7 @@ internal class CheckoutDialog(
) : ComponentDialog(context) {
private var presentedCheckoutWebView: CheckoutWebView? = null
+ private lateinit var keyboardInsets: CheckoutDialogKeyboardInsets
private val backNavigationCallback = object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
@@ -46,15 +52,9 @@ internal class CheckoutDialog(
fun start(context: ComponentActivity) {
log.d(LOG_TAG, "Dialog start called.")
+ window?.configureForCheckoutDialog()
setContentView(R.layout.dialog_checkout)
- window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
- window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
- // Although this flag is deprecated in newest targets, it's properly
- // addressing the keyboard focus on the WebView within the dialog.
- // The non-deprecated alternative (insets listener) does notify about
- // keyboard insets when visible, but it is not adjusting the pan
- // properly into the fields. To be investigated further.
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+ keyboardInsets = CheckoutDialogKeyboardInsets(findViewById(R.id.checkoutKitRoot))
log.d(LOG_TAG, "Finding or creating WebView.")
val checkoutWebView = CheckoutWebView.checkoutViewFor(checkoutUrl, context)
@@ -100,6 +100,13 @@ internal class CheckoutDialog(
log.d(LOG_TAG, "Showing dialog.")
show()
+ // Dialog window size is only applied reliably after show().
+ window?.setLayout(MATCH_PARENT, MATCH_PARENT)
+ keyboardInsets.requestApplyInsets()
+ }
+
+ internal fun applyKeyboardInset(imeBottom: Int) {
+ keyboardInsets.applyKeyboardInset(imeBottom)
}
private fun MenuItem.setupCloseButton(colorScheme: ColorScheme) {
@@ -140,7 +147,7 @@ internal class CheckoutDialog(
findViewById(R.id.checkoutKitContainer).apply {
log.d(LOG_TAG, "Found parent view, setting its colors and layout params.")
setBackgroundColor(colorScheme.webViewBackgroundColor())
- val layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
+ val layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
layoutParams.addRule(RelativeLayout.BELOW, R.id.progressBar)
checkoutWebView.removeFromParent()
log.d(LOG_TAG, "Adding WebView to parent view.")
@@ -200,3 +207,80 @@ internal class CheckoutDialog(
private const val LOG_TAG = "CheckoutDialog"
}
}
+
+internal fun Window.configureForCheckoutDialog() {
+ setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+ setLayout(MATCH_PARENT, MATCH_PARENT)
+ WindowCompat.setDecorFitsSystemWindows(this, false)
+ setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ clearFitInsets()
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private fun Window.clearFitInsets() {
+ attributes = attributes.apply {
+ setFitInsetsTypes(0)
+ setFitInsetsSides(0)
+ }
+}
+
+internal class CheckoutDialogKeyboardInsets(
+ private val root: View,
+) {
+ private val rootPaddingTop = root.paddingTop
+ private val rootPaddingBottom = root.paddingBottom
+
+ init {
+ ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
+ val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val bottomInset = if (imeBottom > 0) imeBottom else systemBars.bottom
+ applyInsets(
+ top = unappliedTopInset(systemBars.top),
+ bottom = unappliedBottomInset(bottomInset),
+ )
+ insets
+ }
+ }
+
+ fun requestApplyInsets() {
+ ViewCompat.requestApplyInsets(root)
+ }
+
+ fun applyKeyboardInset(imeBottom: Int) {
+ applyInsets(top = 0, bottom = imeBottom.coerceAtLeast(0))
+ }
+
+ private fun applyInsets(top: Int, bottom: Int) {
+ root.setPadding(
+ root.paddingLeft,
+ rootPaddingTop + top.coerceAtLeast(0),
+ root.paddingRight,
+ rootPaddingBottom + bottom.coerceAtLeast(0),
+ )
+ }
+
+ private fun unappliedTopInset(inset: Int): Int {
+ if (inset <= 0 || root.height == 0) return inset
+ return if (root.locationOnScreenY() >= inset - INSET_APPLIED_TOLERANCE_PX) 0 else inset
+ }
+
+ private fun unappliedBottomInset(inset: Int): Int {
+ if (inset <= 0 || root.height == 0) return inset
+ val rootBottom = root.locationOnScreenY() + root.height
+ val insetTop = root.resources.displayMetrics.heightPixels - inset
+ return if (rootBottom <= insetTop + INSET_APPLIED_TOLERANCE_PX) 0 else inset
+ }
+
+ private fun View.locationOnScreenY(): Int {
+ val location = IntArray(2)
+ getLocationOnScreen(location)
+ return location[1]
+ }
+
+ private companion object {
+ private const val INSET_APPLIED_TOLERANCE_PX = 2
+ }
+}
diff --git a/platforms/android/lib/src/main/res/layout/dialog_checkout.xml b/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
index 8fca29d13..64376b7df 100644
--- a/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
+++ b/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
@@ -1,8 +1,10 @@
-
+ android:background="@android:color/transparent"
+ android:orientation="vertical">
-
+
diff --git a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
index 4eabd7acd..66b251b3e 100644
--- a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
+++ b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
@@ -4,7 +4,10 @@ import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Looper
import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.WindowManager
import android.webkit.WebView
+import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.activity.ComponentActivity
import androidx.appcompat.widget.Toolbar
@@ -63,6 +66,24 @@ class CheckoutDialogTest {
assertThat(dialog.isShowing).isTrue
}
+ @Test
+ fun `dialog fills height and handles keyboard with insets`() {
+ ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
+
+ val dialog = ShadowDialog.getLatestDialog()
+ val attributes = dialog.window?.attributes
+ val root = dialog.findViewById(R.id.checkoutKitRoot)
+ val container = dialog.findViewById(R.id.checkoutKitContainer)
+ val containerLayoutParams = container.layoutParams as LinearLayout.LayoutParams
+
+ assertThat(root).isInstanceOf(LinearLayout::class.java)
+ assertThat(attributes?.height).isEqualTo(MATCH_PARENT)
+ assertThat(attributes?.softInputMode?.and(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING))
+ .isEqualTo(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
+ assertThat(containerLayoutParams.height).isZero()
+ assertThat(containerLayoutParams.weight).isEqualTo(1f)
+ }
+
@Test
fun `checkoutView is added to the container when dialog is presented`() {
ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
@@ -83,8 +104,26 @@ class CheckoutDialogTest {
val webView: WebView = ShadowDialog.getLatestDialog()
.findViewById(R.id.checkoutKitContainer)
.children.firstOrNull { it is WebView } as WebView? ?: fail("No WebVew found in dialog")
+ val layoutParams = webView.layoutParams as RelativeLayout.LayoutParams
assertThat(shadowOf(webView).wasOnResumeCalled()).isTrue()
+ assertThat(layoutParams.width).isEqualTo(MATCH_PARENT)
+ assertThat(layoutParams.height).isEqualTo(MATCH_PARENT)
+ }
+
+ @Test
+ fun `ime inset changes pad checkout content without resizing window`() {
+ ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
+ val dialog = ShadowDialog.getLatestDialog() as CheckoutDialog
+ val root = dialog.findViewById(R.id.checkoutKitRoot)
+ dialog.window?.setLayout(MATCH_PARENT, 400)
+
+ assertThat(root.paddingBottom).isZero()
+
+ dialog.applyKeyboardInset(100)
+
+ assertThat(dialog.window?.attributes?.height).isEqualTo(400)
+ assertThat(root.paddingBottom).isEqualTo(100)
}
@Test
@@ -238,7 +277,7 @@ class CheckoutDialogTest {
}
@Test
- fun `sets WebView container background color based on current configuration`() {
+ fun `sets checkout content background color based on current configuration`() {
val customColors = customColors()
ShopifyCheckoutKit.configuration.colorScheme = ColorScheme.Web(customColors)
diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/diff.txt b/platforms/android/samples/CheckoutKitAndroidDemo/diff.txt
new file mode 100644
index 000000000..99b9b39e4
--- /dev/null
+++ b/platforms/android/samples/CheckoutKitAndroidDemo/diff.txt
@@ -0,0 +1,272 @@
+diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
+index 173445bc..57fcb733 100644
+--- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
++++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt
+@@ -7,9 +7,10 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
+ import android.graphics.Color
+ import android.os.Build
+ import android.view.MenuItem
++import android.view.View
+ import android.view.View.INVISIBLE
+ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
++import android.view.Window
+ import android.view.WindowManager
+ import android.widget.ProgressBar
+ import android.widget.RelativeLayout
+@@ -17,10 +18,14 @@ import androidx.activity.ComponentActivity
+ import androidx.activity.ComponentDialog
+ import androidx.activity.OnBackPressedCallback
+ import androidx.annotation.ColorInt
++import androidx.annotation.RequiresApi
+ import androidx.appcompat.content.res.AppCompatResources
+ import androidx.appcompat.widget.Toolbar
+ import androidx.core.graphics.drawable.DrawableCompat
+ import androidx.core.graphics.drawable.toDrawable
++import androidx.core.view.ViewCompat
++import androidx.core.view.WindowCompat
++import androidx.core.view.WindowInsetsCompat
+ import androidx.core.view.children
+ import com.shopify.checkoutkit.ShopifyCheckoutKit.log
+
+@@ -32,6 +37,7 @@ internal class CheckoutDialog(
+ ) : ComponentDialog(context) {
+
+ private var presentedCheckoutWebView: CheckoutWebView? = null
++ private lateinit var keyboardInsets: CheckoutDialogKeyboardInsets
+
+ private val backNavigationCallback = object : OnBackPressedCallback(enabled = true) {
+ override fun handleOnBackPressed() {
+@@ -46,15 +52,9 @@ internal class CheckoutDialog(
+
+ fun start(context: ComponentActivity) {
+ log.d(LOG_TAG, "Dialog start called.")
++ window?.configureForCheckoutDialog()
+ setContentView(R.layout.dialog_checkout)
+- window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
+- window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+- // Although this flag is deprecated in newest targets, it's properly
+- // addressing the keyboard focus on the WebView within the dialog.
+- // The non-deprecated alternative (insets listener) does notify about
+- // keyboard insets when visible, but it is not adjusting the pan
+- // properly into the fields. To be investigated further.
+- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
++ keyboardInsets = CheckoutDialogKeyboardInsets(findViewById(R.id.checkoutKitRoot))
+
+ log.d(LOG_TAG, "Finding or creating WebView.")
+ val checkoutWebView = CheckoutWebView.checkoutViewFor(checkoutUrl, context)
+@@ -100,6 +100,13 @@ internal class CheckoutDialog(
+
+ log.d(LOG_TAG, "Showing dialog.")
+ show()
++ // Dialog window size is only applied reliably after show().
++ window?.setLayout(MATCH_PARENT, MATCH_PARENT)
++ keyboardInsets.requestApplyInsets()
++ }
++
++ internal fun applyKeyboardInset(imeBottom: Int) {
++ keyboardInsets.applyKeyboardInset(imeBottom)
+ }
+
+ private fun MenuItem.setupCloseButton(colorScheme: ColorScheme) {
+@@ -140,7 +147,7 @@ internal class CheckoutDialog(
+ findViewById(R.id.checkoutKitContainer).apply {
+ log.d(LOG_TAG, "Found parent view, setting its colors and layout params.")
+ setBackgroundColor(colorScheme.webViewBackgroundColor())
+- val layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
++ val layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ layoutParams.addRule(RelativeLayout.BELOW, R.id.progressBar)
+ checkoutWebView.removeFromParent()
+ log.d(LOG_TAG, "Adding WebView to parent view.")
+@@ -200,3 +207,80 @@ internal class CheckoutDialog(
+ private const val LOG_TAG = "CheckoutDialog"
+ }
+ }
++
++internal fun Window.configureForCheckoutDialog() {
++ setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
++ setLayout(MATCH_PARENT, MATCH_PARENT)
++ WindowCompat.setDecorFitsSystemWindows(this, false)
++ setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
++ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
++ clearFitInsets()
++ }
++}
++
++@RequiresApi(Build.VERSION_CODES.R)
++private fun Window.clearFitInsets() {
++ attributes = attributes.apply {
++ setFitInsetsTypes(0)
++ setFitInsetsSides(0)
++ }
++}
++
++internal class CheckoutDialogKeyboardInsets(
++ private val root: View,
++) {
++ private val rootPaddingTop = root.paddingTop
++ private val rootPaddingBottom = root.paddingBottom
++
++ init {
++ ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
++ val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
++ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
++ val bottomInset = if (imeBottom > 0) imeBottom else systemBars.bottom
++ applyInsets(
++ top = unappliedTopInset(systemBars.top),
++ bottom = unappliedBottomInset(bottomInset),
++ )
++ insets
++ }
++ }
++
++ fun requestApplyInsets() {
++ ViewCompat.requestApplyInsets(root)
++ }
++
++ fun applyKeyboardInset(imeBottom: Int) {
++ applyInsets(top = 0, bottom = imeBottom.coerceAtLeast(0))
++ }
++
++ private fun applyInsets(top: Int, bottom: Int) {
++ root.setPadding(
++ root.paddingLeft,
++ rootPaddingTop + top.coerceAtLeast(0),
++ root.paddingRight,
++ rootPaddingBottom + bottom.coerceAtLeast(0),
++ )
++ }
++
++ private fun unappliedTopInset(inset: Int): Int {
++ if (inset <= 0 || root.height == 0) return inset
++ return if (root.locationOnScreenY() >= inset - INSET_APPLIED_TOLERANCE_PX) 0 else inset
++ }
++
++ private fun unappliedBottomInset(inset: Int): Int {
++ if (inset <= 0 || root.height == 0) return inset
++ val rootBottom = root.locationOnScreenY() + root.height
++ val insetTop = root.resources.displayMetrics.heightPixels - inset
++ return if (rootBottom <= insetTop + INSET_APPLIED_TOLERANCE_PX) 0 else inset
++ }
++
++ private fun View.locationOnScreenY(): Int {
++ val location = IntArray(2)
++ getLocationOnScreen(location)
++ return location[1]
++ }
++
++ private companion object {
++ private const val INSET_APPLIED_TOLERANCE_PX = 2
++ }
++}
+diff --git a/platforms/android/lib/src/main/res/layout/dialog_checkout.xml b/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
+index 8fca29d1..64376b7d 100644
+--- a/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
++++ b/platforms/android/lib/src/main/res/layout/dialog_checkout.xml
+@@ -1,8 +1,10 @@
+-
++ android:background="@android:color/transparent"
++ android:orientation="vertical">
+
+
+
+
+
+
+-
++
+diff --git a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
+index 4eabd7ac..66b251b3 100644
+--- a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
++++ b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutDialogTest.kt
+@@ -4,7 +4,10 @@ import android.app.Dialog
+ import android.graphics.drawable.ColorDrawable
+ import android.os.Looper
+ import android.view.View
++import android.view.ViewGroup.LayoutParams.MATCH_PARENT
++import android.view.WindowManager
+ import android.webkit.WebView
++import android.widget.LinearLayout
+ import android.widget.RelativeLayout
+ import androidx.activity.ComponentActivity
+ import androidx.appcompat.widget.Toolbar
+@@ -63,6 +66,24 @@ class CheckoutDialogTest {
+ assertThat(dialog.isShowing).isTrue
+ }
+
++ @Test
++ fun `dialog fills height and handles keyboard with insets`() {
++ ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
++
++ val dialog = ShadowDialog.getLatestDialog()
++ val attributes = dialog.window?.attributes
++ val root = dialog.findViewById(R.id.checkoutKitRoot)
++ val container = dialog.findViewById(R.id.checkoutKitContainer)
++ val containerLayoutParams = container.layoutParams as LinearLayout.LayoutParams
++
++ assertThat(root).isInstanceOf(LinearLayout::class.java)
++ assertThat(attributes?.height).isEqualTo(MATCH_PARENT)
++ assertThat(attributes?.softInputMode?.and(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING))
++ .isEqualTo(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
++ assertThat(containerLayoutParams.height).isZero()
++ assertThat(containerLayoutParams.weight).isEqualTo(1f)
++ }
++
+ @Test
+ fun `checkoutView is added to the container when dialog is presented`() {
+ ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
+@@ -83,8 +104,26 @@ class CheckoutDialogTest {
+ val webView: WebView = ShadowDialog.getLatestDialog()
+ .findViewById(R.id.checkoutKitContainer)
+ .children.firstOrNull { it is WebView } as WebView? ?: fail("No WebVew found in dialog")
++ val layoutParams = webView.layoutParams as RelativeLayout.LayoutParams
+
+ assertThat(shadowOf(webView).wasOnResumeCalled()).isTrue()
++ assertThat(layoutParams.width).isEqualTo(MATCH_PARENT)
++ assertThat(layoutParams.height).isEqualTo(MATCH_PARENT)
++ }
++
++ @Test
++ fun `ime inset changes pad checkout content without resizing window`() {
++ ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
++ val dialog = ShadowDialog.getLatestDialog() as CheckoutDialog
++ val root = dialog.findViewById(R.id.checkoutKitRoot)
++ dialog.window?.setLayout(MATCH_PARENT, 400)
++
++ assertThat(root.paddingBottom).isZero()
++
++ dialog.applyKeyboardInset(100)
++
++ assertThat(dialog.window?.attributes?.height).isEqualTo(400)
++ assertThat(root.paddingBottom).isEqualTo(100)
+ }
+
+ @Test
+@@ -238,7 +277,7 @@ class CheckoutDialogTest {
+ }
+
+ @Test
+- fun `sets WebView container background color based on current configuration`() {
++ fun `sets checkout content background color based on current configuration`() {
+ val customColors = customColors()
+ ShopifyCheckoutKit.configuration.colorScheme = ColorScheme.Web(customColors)
+