From d51259ca301ac443a0867572d2c254b1ddd618af Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 20 Mar 2026 12:38:44 +0200 Subject: [PATCH 1/4] DeepLinkConfiguration --- ...PaymentDeepLinkConfigurationJsonAdapter.kt | 35 +++++++++++++++++++ .../v2/PONativeAlternativePaymentRedirect.kt | 15 +++++++- .../com/processout/sdk/di/NetworkGraph.kt | 3 +- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/adapter/napm/NativeAlternativePaymentDeepLinkConfigurationJsonAdapter.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/adapter/napm/NativeAlternativePaymentDeepLinkConfigurationJsonAdapter.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/adapter/napm/NativeAlternativePaymentDeepLinkConfigurationJsonAdapter.kt new file mode 100644 index 000000000..f10f7b6ad --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/adapter/napm/NativeAlternativePaymentDeepLinkConfigurationJsonAdapter.kt @@ -0,0 +1,35 @@ +package com.processout.sdk.api.model.adapter.napm + +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect +import com.squareup.moshi.FromJson +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.squareup.moshi.ToJson + +internal class NativeAlternativePaymentDeepLinkConfigurationJsonAdapter { + + @FromJson + fun fromJson(configuration: DeepLinkConfiguration) = + PONativeAlternativePaymentRedirect.DeepLinkConfiguration( + packageNames = configuration.android.packageNames + ) + + @ToJson + fun toJson(configuration: PONativeAlternativePaymentRedirect.DeepLinkConfiguration) = + DeepLinkConfiguration( + android = DeepLinkConfiguration.Android( + packageNames = configuration.packageNames + ) + ) + + @JsonClass(generateAdapter = true) + data class DeepLinkConfiguration( + val android: Android + ) { + @JsonClass(generateAdapter = true) + data class Android( + @Json(name = "package_names") + val packageNames: Set + ) + } +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt index 2ed975752..8896d93d9 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/napm/v2/PONativeAlternativePaymentRedirect.kt @@ -21,6 +21,7 @@ import kotlinx.parcelize.Parcelize * in the [PONativeAlternativePaymentAuthorizationRequest.redirectConfirmation] * or the [PONativeAlternativePaymentTokenizationRequest.redirectConfirmation], * depending on the flow. + * @param[deepLinkConfiguration] Additional deep link configuration. */ @Parcelize @JsonClass(generateAdapter = true) @@ -30,7 +31,9 @@ data class PONativeAlternativePaymentRedirect( @Json(name = "type") val rawType: String, @Json(name = "confirmation_required") - val confirmationRequired: Boolean + val confirmationRequired: Boolean, + @Json(name = "deep_link") + val deepLinkConfiguration: DeepLinkConfiguration? ) : Parcelable { /** Redirect type. */ @@ -54,4 +57,14 @@ data class PONativeAlternativePaymentRedirect( @ProcessOutInternalApi UNKNOWN(String()) } + + /** + * Specifies additional deep link configuration. + * + * @param[packageNames] A set of app package names the deep link is intended to open. + */ + @Parcelize + data class DeepLinkConfiguration( + val packageNames: Set + ) : Parcelable } diff --git a/sdk/src/main/kotlin/com/processout/sdk/di/NetworkGraph.kt b/sdk/src/main/kotlin/com/processout/sdk/di/NetworkGraph.kt index 21982bae2..1cdf91686 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/di/NetworkGraph.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/di/NetworkGraph.kt @@ -1,5 +1,6 @@ package com.processout.sdk.di +import com.processout.sdk.api.model.adapter.napm.NativeAlternativePaymentDeepLinkConfigurationJsonAdapter import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod import com.processout.sdk.api.model.response.PODynamicCheckoutPaymentMethod.* import com.processout.sdk.api.model.response.napm.v2.NativeAlternativePaymentElement @@ -85,7 +86,7 @@ internal class DefaultNetworkGraph( .withSubtype(PONativeAlternativePaymentCustomerInstruction.Image::class.java, "image_url") .withSubtype(PONativeAlternativePaymentCustomerInstruction.Barcode::class.java, "barcode") .withDefaultValue(PONativeAlternativePaymentCustomerInstruction.Unknown) - ) + ).add(NativeAlternativePaymentDeepLinkConfigurationJsonAdapter()) private fun Moshi.Builder.addDynamicCheckoutAdapter() = add( From b4ede7123518e64caf8c2ef91cb11caa79b6ec8b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 20 Mar 2026 13:42:10 +0200 Subject: [PATCH 2/4] UriExtensions --- .../com/processout/sdk/ui/shared/extension/UriExtensions.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/UriExtensions.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/UriExtensions.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/UriExtensions.kt new file mode 100644 index 000000000..d3e726e1d --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/UriExtensions.kt @@ -0,0 +1,6 @@ +package com.processout.sdk.ui.shared.extension + +import android.net.Uri + +internal val Uri.isWeb: Boolean + get() = scheme?.lowercase() in setOf("http", "https") From 7f393be52c4806bd06ce0e032f6190f77adfe66c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 20 Mar 2026 14:48:01 +0200 Subject: [PATCH 3/4] openDeepLink() with package names --- .../shared/extension/ApplicationExtensions.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt index 8c787d609..1ac5e584e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/ApplicationExtensions.kt @@ -14,14 +14,34 @@ internal fun Application.currentAppLocale(): Locale = ?: LocaleListCompat.getAdjustedDefault()[0] ?: Locale.getDefault() -internal fun Application.openDeepLink(url: String): Boolean { +internal fun Application.openDeepLink( + url: String, + packageName: String? = null +): Boolean { try { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + packageName?.let { intent.setPackage(it) } startActivity(intent) return true } catch (e: ActivityNotFoundException) { - POLogger.warn("Failed to open deep link [%s] with exception: %s", url, e) + POLogger.warn("Failed to open deep link [%s] for package [%s] with exception: %s", url, packageName, e) return false } } + +internal fun Application.openDeepLink( + url: String, + packageNames: Set? +): Boolean { + if (packageNames.isNullOrEmpty()) { + if (url.toUri().isWeb) { + POLogger.warn("Refused to open app link [%s] without a package, to prevent opening in an external browser.", url) + return false + } + return openDeepLink(url) + } + return packageNames.any { packageName -> + openDeepLink(url, packageName) + } +} From b010b2b3ea26503a80c2f9c2802d00731b3a967e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 20 Mar 2026 15:46:52 +0200 Subject: [PATCH 4/4] Pass app package names when opening deep link --- .../ui/napm/NativeAlternativePaymentInteractor.kt | 12 +++++++++--- .../ui/napm/PONativeAlternativePaymentLauncher.kt | 8 +++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 357526141..07aaf3c10 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -31,6 +31,7 @@ import com.processout.sdk.api.model.response.napm.v2.* import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentAuthorizationResponse.Invoice import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentElement.Form.Parameter.Otp.Subtype +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.DeepLinkConfiguration import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.RedirectType import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.* import com.processout.sdk.api.service.POCustomerTokensService @@ -672,7 +673,8 @@ internal class NativeAlternativePaymentInteractor( ) RedirectType.DEEP_LINK -> deepLinkRedirect( stateValue = stateValue, - redirectUrl = redirect.url + redirectUrl = redirect.url, + deepLinkConfiguration = redirect.deepLinkConfiguration ) RedirectType.UNKNOWN -> failWithUnknownRedirect(redirect) } @@ -727,7 +729,8 @@ internal class NativeAlternativePaymentInteractor( private fun deepLinkRedirect( stateValue: NextStepStateValue, - redirectUrl: String + redirectUrl: String, + deepLinkConfiguration: DeepLinkConfiguration? ) { _state.update { NextStep( @@ -737,7 +740,10 @@ internal class NativeAlternativePaymentInteractor( ) ) } - val didOpenUrl = app.openDeepLink(url = redirectUrl) + val didOpenUrl = app.openDeepLink( + url = redirectUrl, + packageNames = deepLinkConfiguration?.packageNames + ) val redirectConfirmation = if (stateValue.redirect?.confirmationRequired == true) PONativeAlternativePaymentRedirectConfirmation(success = didOpenUrl) else null continuePayment(redirectConfirmation) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt index 280aee0c1..9e5d4aca0 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentLauncher.kt @@ -18,6 +18,7 @@ import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRe import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentTokenizationRequest import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect +import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.DeepLinkConfiguration import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentRedirect.RedirectType import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState import com.processout.sdk.api.model.response.napm.v2.PONativeAlternativePaymentState.* @@ -402,6 +403,7 @@ class PONativeAlternativePaymentLauncher private constructor( ) RedirectType.DEEP_LINK -> deepLinkRedirect( redirectUrl = redirect.url, + deepLinkConfiguration = redirect.deepLinkConfiguration, configuration = configuration ) RedirectType.UNKNOWN -> { @@ -471,9 +473,13 @@ class PONativeAlternativePaymentLauncher private constructor( private fun deepLinkRedirect( redirectUrl: String, + deepLinkConfiguration: DeepLinkConfiguration?, configuration: PONativeAlternativePaymentConfiguration ) { - val didOpenUrl = app.openDeepLink(url = redirectUrl) + val didOpenUrl = app.openDeepLink( + url = redirectUrl, + packageNames = deepLinkConfiguration?.packageNames + ) val confirmationRequired = when (val flow = configuration.flow) { is Authorization -> flow.initialResponse?.redirect?.confirmationRequired is Tokenization -> flow.initialResponse?.redirect?.confirmationRequired