From b93479c88ec1875942dc599ad32b963ca5a9a20e Mon Sep 17 00:00:00 2001 From: Fabian Bender Date: Tue, 26 Aug 2025 15:13:18 +0200 Subject: [PATCH 1/2] WIP: provide authentication fragment if strong customer authorization is activated --- .../java/io/snabble/sdk/checkout/Checkout.kt | 12 ++- .../io/snabble/sdk/checkout/CheckoutApi.kt | 4 + .../io/snabble/sdk/checkout/CheckoutState.kt | 6 ++ .../sdk/ui/checkout/AuthenticationFragment.kt | 85 +++++++++++++++++++ .../sdk/ui/checkout/CheckoutActivity.kt | 2 + .../snabble_fragment_authentication.xml | 11 +++ .../res/navigation/snabble_nav_checkout.xml | 6 ++ 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt create mode 100644 ui/src/main/res/layout/snabble_fragment_authentication.xml diff --git a/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt b/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt index fa617f0481..2c90a00001 100644 --- a/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt +++ b/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt @@ -3,14 +3,18 @@ package io.snabble.sdk.checkout import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData -import io.snabble.sdk.* +import io.snabble.sdk.FulfillmentState +import io.snabble.sdk.MutableAccessibleLiveData +import io.snabble.sdk.PaymentMethod +import io.snabble.sdk.Product +import io.snabble.sdk.Project +import io.snabble.sdk.Snabble import io.snabble.sdk.Snabble.instance import io.snabble.sdk.payment.PaymentCredentials import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.utils.Dispatch import io.snabble.sdk.utils.Logger import java.io.File -import java.util.* import java.util.concurrent.Future class Checkout @JvmOverloads constructor( @@ -580,6 +584,10 @@ class Checkout @JvmOverloads constructor( notifyStateChanged(CheckoutState.PAYMENT_PROCESSING) } + CheckState.AUTHENTICATING -> { + notifyStateChanged(CheckoutState.AUTHENTICATING) + } + CheckState.SUCCESSFUL -> { val exitToken = checkoutProcess.exitToken return if (exitToken != null && (exitToken.format.isNullOrEmpty() || exitToken.value.isNullOrEmpty())) { diff --git a/core/src/main/java/io/snabble/sdk/checkout/CheckoutApi.kt b/core/src/main/java/io/snabble/sdk/checkout/CheckoutApi.kt index d47e059bf7..3bb5d94774 100644 --- a/core/src/main/java/io/snabble/sdk/checkout/CheckoutApi.kt +++ b/core/src/main/java/io/snabble/sdk/checkout/CheckoutApi.kt @@ -119,6 +119,7 @@ enum class CheckState { @SerializedName("unauthorized") UNAUTHORIZED, @SerializedName("pending") PENDING, @SerializedName("processing") PROCESSING, + @SerializedName("authenticating") AUTHENTICATING, @SerializedName("successful") SUCCESSFUL, @SerializedName("transferred") TRANSFERRED, @SerializedName("failed") FAILED @@ -386,6 +387,9 @@ data class CheckoutProcessResponse( val authorizePaymentLink: String? get() = links?.get("authorizePayment")?.href + val paymentRedirect: String? + get() = links?.get("paymentRedirect")?.href + val originCandidateLink: String? get() = paymentResult?.originCandidateLink } diff --git a/core/src/main/java/io/snabble/sdk/checkout/CheckoutState.kt b/core/src/main/java/io/snabble/sdk/checkout/CheckoutState.kt index 5cc0df7893..f2d15b11e6 100644 --- a/core/src/main/java/io/snabble/sdk/checkout/CheckoutState.kt +++ b/core/src/main/java/io/snabble/sdk/checkout/CheckoutState.kt @@ -26,6 +26,12 @@ enum class CheckoutState { */ VERIFYING_PAYMENT_METHOD, + /** + * The payment need further authentication. + * Use the paymentRedirect link provided for further authentication. + */ + AUTHENTICATING, + /** * Age needs to be verified */ diff --git a/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt b/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt new file mode 100644 index 0000000000..a66aec30c5 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt @@ -0,0 +1,85 @@ +package io.snabble.sdk.ui.checkout + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.Fragment +import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.R +import io.snabble.sdk.utils.Logger + +class AuthenticationFragment : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.snabble_fragment_authentication, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val currentCheckout = Snabble.checkedInProject.value?.checkout + + // Setup your views here after inflation + val webview = view.findViewById(R.id.authentication_webview) + webview.setupCookies() + + // Configure WebView settings + webview.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + loadWithOverviewMode = true + useWideViewPort = true + setSupportZoom(true) + builtInZoomControls = true + displayZoomControls = false + + // Additional cookie-related settings + databaseEnabled = true + cacheMode = WebSettings.LOAD_DEFAULT + } + + webview.webViewClient = object : WebViewClient() { + override fun onReceivedError(view: WebView?, errorCode: Int, description: String?, failingUrl: String?) { + Logger.d("onReceivedError $failingUrl") + } + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + when (request.url.toString()) { + SUCCESS, + ERROR, + BACK -> { + // Check if the we need to do something, since the checkout activity is observing the state and should handle redirects itself + // If not we need something like popBackstack and maybe notify the user with a toast + return true + } + } + return super.shouldOverrideUrlLoading(view, request) + } + } + + val redirectUrl = currentCheckout?.checkoutProcess?.paymentRedirect + redirectUrl?.let { + webview.loadUrl(it) + } + } + + // Extension function for cleaner code + fun WebView.setupCookies() { + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) + cookieManager.setAcceptThirdPartyCookies(this, true) + cookieManager.flush() + } + + private companion object { + + const val SUCCESS = "snabble://payone/success" + const val ERROR = "snabble://payone/error" + const val BACK = "snabble://payone/back" + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutActivity.kt b/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutActivity.kt index cdf873365b..ddb447e64d 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutActivity.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutActivity.kt @@ -214,6 +214,8 @@ class CheckoutActivity : FragmentActivity() { } } + CheckoutState.AUTHENTICATING -> R.id.snabble_nav_authentication + CheckoutState.DEPOSIT_RETURN_REDEMPTION_FAILED, CheckoutState.PAYMENT_ABORTED -> { finish() diff --git a/ui/src/main/res/layout/snabble_fragment_authentication.xml b/ui/src/main/res/layout/snabble_fragment_authentication.xml new file mode 100644 index 0000000000..aca169a277 --- /dev/null +++ b/ui/src/main/res/layout/snabble_fragment_authentication.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/ui/src/main/res/navigation/snabble_nav_checkout.xml b/ui/src/main/res/navigation/snabble_nav_checkout.xml index d922abee64..97eb45745a 100644 --- a/ui/src/main/res/navigation/snabble_nav_checkout.xml +++ b/ui/src/main/res/navigation/snabble_nav_checkout.xml @@ -37,6 +37,12 @@ android:name="io.snabble.sdk.ui.checkout.PaymentStatusFragment" android:label="@string/Snabble.Checkout.title" /> + + Date: Wed, 17 Sep 2025 14:42:51 +0200 Subject: [PATCH 2/2] keep polling if the checkout state is Authentication --- .../java/io/snabble/sdk/checkout/Checkout.kt | 1 + .../sdk/ui/checkout/AuthenticationFragment.kt | 18 ++++++------------ .../snabble/sdk/ui/checkout/CheckoutHelper.kt | 1 + .../res/navigation/snabble_nav_checkout.xml | 3 +-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt b/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt index 2c90a00001..54401b7507 100644 --- a/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt +++ b/core/src/main/java/io/snabble/sdk/checkout/Checkout.kt @@ -503,6 +503,7 @@ class Checkout @JvmOverloads constructor( || state == CheckoutState.PAYMENT_PROCESSING || (state == CheckoutState.PAYMENT_APPROVED && !areAllFulfillmentsClosed()) || state == CheckoutState.PAYONE_SEPA_MANDATE_REQUIRED + || state == CheckoutState.AUTHENTICATING ) { scheduleNextPoll() } diff --git a/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt b/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt index a66aec30c5..b917845a64 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/checkout/AuthenticationFragment.kt @@ -1,26 +1,20 @@ package io.snabble.sdk.ui.checkout import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.webkit.CookieManager import android.webkit.WebResourceRequest import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient -import androidx.fragment.app.Fragment import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.BaseFragment import io.snabble.sdk.ui.R import io.snabble.sdk.utils.Logger -class AuthenticationFragment : Fragment() { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(R.layout.snabble_fragment_authentication, container, false) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) +class AuthenticationFragment : BaseFragment(R.layout.snabble_fragment_authentication) { + override fun onActualViewCreated(view: View, savedInstanceState: Bundle?) { + super.onActualViewCreated(view, savedInstanceState) val currentCheckout = Snabble.checkedInProject.value?.checkout @@ -53,8 +47,8 @@ class AuthenticationFragment : Fragment() { SUCCESS, ERROR, BACK -> { - // Check if the we need to do something, since the checkout activity is observing the state and should handle redirects itself - // If not we need something like popBackstack and maybe notify the user with a toast + // No need to pop it since we're polling for the checkout state in the activity + // and navigate away as soon the get an update state return true } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutHelper.kt b/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutHelper.kt index 7e812d2661..83a07dc920 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutHelper.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/checkout/CheckoutHelper.kt @@ -22,6 +22,7 @@ val CheckoutState.isCheckoutState: Boolean CheckoutState.WAIT_FOR_GATEKEEPER, CheckoutState.WAIT_FOR_APPROVAL, CheckoutState.PAYMENT_PROCESSING, + CheckoutState.AUTHENTICATING, CheckoutState.PAYMENT_APPROVED, CheckoutState.DENIED_TOO_YOUNG, CheckoutState.DENIED_BY_PAYMENT_PROVIDER, diff --git a/ui/src/main/res/navigation/snabble_nav_checkout.xml b/ui/src/main/res/navigation/snabble_nav_checkout.xml index 97eb45745a..d547782538 100644 --- a/ui/src/main/res/navigation/snabble_nav_checkout.xml +++ b/ui/src/main/res/navigation/snabble_nav_checkout.xml @@ -40,8 +40,7 @@ + android:label="@string/Snabble.Checkout.title" />