Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension

buildscript {
ext {
androidGradlePluginVersion = '8.13.2'
androidGradlePluginVersion = '9.1.0'
kotlinVersion = '2.1.20'
kspVersion = '2.1.20-1.0.32'
dokkaVersion = '1.9.20'
Expand Down
11 changes: 11 additions & 0 deletions example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
<activity
android:name=".redirect.MerchantRedirectActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="merchant-example.com"
android:pathPrefix="/return"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ class MerchantRedirectActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ProcessOut.instance.processDeepLink(
activity = this,
uri = intent.data
)
intent.data?.let { uri ->
ProcessOut.instance.processDeepLink(hostActivity = this, uri)
}
finish()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.processout.example.shared
import com.processout.example.BuildConfig

object Constants {
const val DEFAULT_RETURN_URL = "${BuildConfig.APPLICATION_ID}://processout/return"
const val MERCHANT_RETURN_URL = "merchant://example/return"
const val RETURN_URL_DEFAULT = "${BuildConfig.APPLICATION_ID}://processout/return"
const val RETURN_URL_APP_LINK = "https://merchant-example.com/return"
const val RETURN_URL_DEEP_LINK = "merchant://example/return"
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
delegate = Netcetera3DS2ServiceDelegate(
provideActivity = { POCardTokenizationActivity.instance },
customTabLauncher = customTabLauncher,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
),
configuration = PONetcetera3DS2ServiceConfiguration(
bridgingExtensionVersion = BridgingMessageExtensionVersion.V20
Expand All @@ -130,7 +130,7 @@ class CardPaymentFragment : BaseFragment<FragmentCardPaymentBinding>(
delegate = Checkout3DSServiceDelegate(
activity = requireActivity(),
customTabLauncher = customTabLauncher,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
)
).build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class CardPaymentViewModel(
amount = state.amount,
currency = state.currency,
customerId = state.customerId,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
)
).getOrNull()
_state.update { it.copy(invoiceId = invoice?.id) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class DefaultDynamicCheckoutDelegate(
amount = details.amount,
currency = details.currency,
customerId = customerId,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class DynamicCheckoutFragment : BaseFragment<FragmentDynamicCheckoutBinding>(
clientSecret = uiModel.clientSecret
),
alternativePayment = AlternativePaymentConfiguration(
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
)
)
)
Expand All @@ -111,7 +111,7 @@ class DynamicCheckoutFragment : BaseFragment<FragmentDynamicCheckoutBinding>(
delegate = Netcetera3DS2ServiceDelegate(
provideActivity = { PODynamicCheckoutActivity.instance },
customTabLauncher = customTabLauncher,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
),
configuration = PONetcetera3DS2ServiceConfiguration(
bridgingExtensionVersion = BridgingMessageExtensionVersion.V20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class DynamicCheckoutViewModel(
amount = details.amount,
currency = details.currency,
customerId = customerId,
returnUrl = Constants.DEFAULT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEFAULT
)
).onSuccess { invoice ->
_uiState.value = Submitted(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ class NativeApmFragment : BaseFragment<FragmentNativeApmBinding>(
),
cancelButton = CancelButton(),
redirect = RedirectConfiguration(
returnUrl = Constants.MERCHANT_RETURN_URL,
enableHeadlessMode = true
returnUrl = Constants.RETURN_URL_DEEP_LINK,
enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked
),
paymentConfirmation = PaymentConfirmationConfiguration(
confirmButton = Button(),
Expand All @@ -138,8 +138,8 @@ class NativeApmFragment : BaseFragment<FragmentNativeApmBinding>(
),
cancelButton = CancelButton(),
redirect = RedirectConfiguration(
returnUrl = Constants.MERCHANT_RETURN_URL,
enableHeadlessMode = true
returnUrl = Constants.RETURN_URL_DEEP_LINK,
enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked
),
paymentConfirmation = PaymentConfirmationConfiguration(
confirmButton = Button(),
Expand All @@ -163,7 +163,8 @@ class NativeApmFragment : BaseFragment<FragmentNativeApmBinding>(
),
cancelButton = CancelButton(),
redirect = RedirectConfiguration(
returnUrl = Constants.MERCHANT_RETURN_URL
returnUrl = Constants.RETURN_URL_DEEP_LINK,
enableHeadlessMode = binding.enableHeadlessModeCheckbox.isChecked
),
paymentConfirmation = PaymentConfirmationConfiguration(
confirmButton = Button(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class NativeApmViewModel(
amount = amount,
currency = currency,
customerId = customerId,
returnUrl = Constants.MERCHANT_RETURN_URL,
returnUrl = Constants.RETURN_URL_DEEP_LINK,
shipping = POContact(
address1 = "6th Street",
city = "Paris",
Expand Down
9 changes: 9 additions & 0 deletions example/src/main/res/layout/fragment_native_apm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@
android:inputType="text|textCapCharacters" />
</com.google.android.material.textfield.TextInputLayout>

<CheckBox
android:id="@+id/enable_headless_mode_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:checked="false"
android:text="Enable headless mode"
android:textSize="16sp" />

<TextView
android:id="@+id/customer"
android:layout_width="match_parent"
Expand Down
10 changes: 10 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Wed Oct 05 19:43:19 EEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
29 changes: 22 additions & 7 deletions sdk/src/main/kotlin/com/processout/sdk/api/ProcessOut.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
package com.processout.sdk.api

import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.processout.processout_sdk.ProcessOutLegacyAccessor
import com.processout.sdk.BuildConfig
import com.processout.sdk.api.dispatcher.PODefaultEventDispatchers
import com.processout.sdk.api.dispatcher.POEventDispatcher
import com.processout.sdk.api.dispatcher.POEventDispatchers
import com.processout.sdk.api.dispatcher.PONativeAlternativePaymentMethodEventDispatcher
import com.processout.sdk.api.model.event.PODeepLinkReceivedEvent
import com.processout.sdk.api.network.ApiConstants
import com.processout.sdk.api.preferences.Preferences
import com.processout.sdk.api.repository.POCardsRepository
Expand All @@ -21,6 +24,7 @@ import com.processout.sdk.api.service.POInvoicesService
import com.processout.sdk.core.logger.POLogger
import com.processout.sdk.di.*
import com.processout.sdk.ui.web.customtab.POCustomTabAuthorizationActivity
import kotlinx.coroutines.launch

/**
* Entry point to ProcessOut Android SDK.
Expand Down Expand Up @@ -76,15 +80,26 @@ class ProcessOut private constructor(

/**
* Processes an incoming deep/app link received by your app.
* Call this method even when [uri] is _null_ to complete the flow correctly.
*/
@Deprecated(message = "Use alternative function.")
fun processDeepLink(activity: Activity, uri: Uri?) {
POLogger.info("Processing deep link: %s", uri)
val intent = Intent(activity, POCustomTabAuthorizationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.data = uri
activity.startActivity(intent)
val componentActivity = activity as? ComponentActivity
if (componentActivity != null && uri != null) {
processDeepLink(hostActivity = componentActivity, uri)
} else {
POCustomTabAuthorizationActivity.redirect(hostActivity = activity, uri)
}
}

/**
* Processes an incoming deep/app link received by your app.
*/
fun processDeepLink(hostActivity: ComponentActivity, uri: Uri) {
POLogger.info("Processing deep link: %s", uri)
hostActivity.lifecycleScope.launch {
POEventDispatcher.instance.send(event = PODeepLinkReceivedEvent(uri))
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import java.util.UUID
import kotlin.reflect.KClass

/** @suppress */
@ProcessOutInternalApi
Expand All @@ -31,6 +32,9 @@ class POEventDispatcher {
private val _events = MutableSharedFlow<Any>()
val events = _events.asSharedFlow()

@PublishedApi
internal val subscriptions = mutableSetOf<KClass<*>>()

private val _requests = MutableSharedFlow<Request>()
val requests = _requests.asSharedFlow()

Expand All @@ -53,15 +57,29 @@ class POEventDispatcher {
coroutineScope: CoroutineScope,
crossinline onEvent: (T) -> Unit
) {
synchronized(subscriptions) {
subscriptions.add(T::class)
}
coroutineScope.launch {
events.filterIsInstance<T>()
.collect { event ->
coroutineContext.ensureActive()
onEvent(event)
try {
events.filterIsInstance<T>()
.collect { event ->
coroutineContext.ensureActive()
onEvent(event)
}
} finally {
synchronized(subscriptions) {
subscriptions.remove(T::class)
}
}
}
}

inline fun <reified T : Any> hasSubscribers(): Boolean =
synchronized(subscriptions) {
subscriptions.contains(T::class)
}

inline fun <reified T : Request> subscribeForRequest(
coroutineScope: CoroutineScope,
crossinline onRequest: (T) -> Unit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.processout.sdk.api.model.event

import android.net.Uri
import com.processout.sdk.core.annotation.ProcessOutInternalApi

/** @suppress */
@ProcessOutInternalApi
data class PODeepLinkReceivedEvent(
val uri: Uri
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import com.squareup.moshi.JsonClass
internal data class NativeAlternativePaymentRequestBody(
@Json(name = "gateway_configuration_id")
val gatewayConfigurationId: String,
val configuration: Configuration,
val source: String?,
@Json(name = "submit_data")
val submitData: SubmitData?,
@Json(name = "redirect")
val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation?
) {

@JsonClass(generateAdapter = true)
data class Configuration(
@Json(name = "return_redirect_type")
val returnRedirectType: String
)

@JsonClass(generateAdapter = true)
data class SubmitData(
val parameters: Map<String, Any>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ package com.processout.sdk.api.model.request.napm.v2
*
* @param[invoiceId] Invoice identifier.
* @param[gatewayConfigurationId] Gateway configuration identifier.
* @param[configuration] Payment configuration.
* __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls.
* @param[source] Payment source.
* @param[submitData] Payment payload.
* @param[redirectConfirmation] Redirect confirmation.
*/
data class PONativeAlternativePaymentAuthorizationRequest(
val invoiceId: String,
val gatewayConfigurationId: String,
val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(),
val source: String? = null,
val submitData: PONativeAlternativePaymentSubmitData? = null,
val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.processout.sdk.api.model.request.napm.v2

import com.processout.sdk.api.model.request.napm.v2.PONativeAlternativePaymentRequestConfiguration.ReturnRedirectType.AUTOMATIC
import com.squareup.moshi.JsonClass

/**
* Payment configuration.
*
* @param[returnRedirectType] Return redirect type. By default [AUTOMATIC].
*/
data class PONativeAlternativePaymentRequestConfiguration(
val returnRedirectType: ReturnRedirectType = AUTOMATIC
) {

/**
* Return redirect type.
*/
@JsonClass(generateAdapter = false)
enum class ReturnRedirectType(val rawValue: String) {
/** Redirect result is handled automatically. */
AUTOMATIC("automatic"),

/** Redirect result is not processed automatically and should be resolved explicitly. */
MANUAL("manual")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ package com.processout.sdk.api.model.request.napm.v2
* @param[customerId] Customer identifier.
* @param[customerTokenId] Customer token identifier.
* @param[gatewayConfigurationId] Gateway configuration identifier.
* @param[configuration] Payment configuration.
* __Note:__ Configuration is respected _only on the first_ payment request and ignored on subsequent calls.
* @param[submitData] Payment payload.
* @param[redirectConfirmation] Redirect confirmation.
*/
data class PONativeAlternativePaymentTokenizationRequest(
val customerId: String,
val customerTokenId: String,
val gatewayConfigurationId: String,
val configuration: PONativeAlternativePaymentRequestConfiguration = PONativeAlternativePaymentRequestConfiguration(),
val submitData: PONativeAlternativePaymentSubmitData? = null,
val redirectConfirmation: PONativeAlternativePaymentRedirectConfirmation? = null
)
Loading
Loading