diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt index c23d65c6d..e854634d6 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt @@ -417,14 +417,24 @@ internal class CheckoutWebView private constructor( return when { uri == null -> false uri.isContactLink() || uri.isDeepLink() -> { - when (val result = ExternalUriLauncher.launchExternalApp(context, uri)) { - is ExternalUriLauncher.Result.Launched -> - log.d(LOG_TAG, "Deep link intercepted: ${uri.redactedForLogging()} — allowed") - is ExternalUriLauncher.Result.Rejected -> - log.d( - LOG_TAG, - "Deep link intercepted: ${uri.redactedForLogging()} — rejected (${result.reason})" - ) + log.d(LOG_TAG, "Raw navigation link intercepted: ${uri.redactedForLogging()}") + when (listener.onCheckoutLinkClicked(CheckoutLink(uri))) { + CheckoutLinkAction.Open -> { + log.d(LOG_TAG, "Raw navigation link delegated to SDK default handler") + when (val result = ExternalUriLauncher.launchExternalApp(context, uri)) { + is ExternalUriLauncher.Result.Launched -> + log.d(LOG_TAG, "Raw navigation link opened with SDK default handler") + is ExternalUriLauncher.Result.Rejected -> + log.d( + LOG_TAG, + "Raw navigation link rejected by SDK default handler: ${result.reason}" + ) + } + } + CheckoutLinkAction.Handled -> + log.d(LOG_TAG, "Raw navigation link handled by host") + CheckoutLinkAction.Cancel -> + log.d(LOG_TAG, "Raw navigation link canceled by host") } true } diff --git a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutWebViewClientTest.kt b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutWebViewClientTest.kt index aa384a46d..561057597 100644 --- a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutWebViewClientTest.kt +++ b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutWebViewClientTest.kt @@ -50,6 +50,7 @@ class CheckoutWebViewClientTest { // no activity resolves the intent. Robolectric defaults to silently recording the // intent instead — turning on checkActivities aligns the shadow with production. shadowOf(activity.application).checkActivities(true) + whenever(mockListener.onCheckoutLinkClicked(any())).thenReturn(CheckoutLinkAction.Open) } @After @@ -104,6 +105,50 @@ class CheckoutWebViewClientTest { assertThat(shadowOf(activity).nextStartedActivity).isNotNull } + @Test + fun `delegates deep links to the listener before opening them`() { + val uri = Uri.parse("geo:40.712776,-74.005974?q=Statue+of+Liberty") + registerResolverFor(uri) + val mockRequest = mockWebRequest(uri) + + val view = viewWithProcessor(activity) + val overridden = view.CheckoutWebViewClient().shouldOverrideUrlLoading(view, mockRequest) + + assertThat(overridden).isTrue + val link = argumentCaptor() + verify(mockListener).onCheckoutLinkClicked(link.capture()) + assertThat(link.firstValue.url).isEqualTo(uri) + assertThat(shadowOf(activity).nextStartedActivity).isNotNull + } + + @Test + fun `does not open a deep link handled by the listener`() { + val listener = mock() + whenever(listener.onCheckoutLinkClicked(any())).thenReturn(CheckoutLinkAction.Handled) + val uri = Uri.parse("myapp://path") + val view = viewWithProcessor(activity, CheckoutWebViewListener(listener)) + + val overridden = view.CheckoutWebViewClient().shouldOverrideUrlLoading(view, mockWebRequest(uri)) + + assertThat(overridden).isTrue + verify(listener).onCheckoutLinkClicked(any()) + assertThat(shadowOf(activity).peekNextStartedActivityForResult()).isNull() + } + + @Test + fun `does not open a deep link canceled by the listener`() { + val listener = mock() + whenever(listener.onCheckoutLinkClicked(any())).thenReturn(CheckoutLinkAction.Cancel) + val uri = Uri.parse("myapp://path") + val view = viewWithProcessor(activity, CheckoutWebViewListener(listener)) + + val overridden = view.CheckoutWebViewClient().shouldOverrideUrlLoading(view, mockWebRequest(uri)) + + assertThat(overridden).isTrue + verify(listener).onCheckoutLinkClicked(any()) + assertThat(shadowOf(activity).peekNextStartedActivityForResult()).isNull() + } + @Test fun `overrides url loading for unresolvable deep link but launches no intent`() { val mockRequest = mockWebRequest(Uri.parse("myapp://path")) diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt index 8552cf9ee..dc86dc957 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt @@ -305,10 +305,13 @@ class CartViewModel( activity: ComponentActivity, handler: WindowOpenHandler, ): CheckoutLinkAction { + recordSdkEvent("Checkout link clicked", mapOf("url" to link.url.toString())) // Returning Open keeps Checkout Kit's Custom Tab and non-web intent handling. - if (handler == WindowOpenHandler.Default) return CheckoutLinkAction.Open + if (handler == WindowOpenHandler.Default) { + recordSdkEvent("Checkout link delegated to Checkout Kit", mapOf("url" to link.url.toString())) + return CheckoutLinkAction.Open + } - recordSdkEvent("Checkout link clicked", mapOf("url" to link.url.toString())) return try { val intent = Intent(Intent.ACTION_VIEW, link.url).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) activity.startActivity(intent)